Skip to content

Commit

Permalink
fix(enterprise): fix login flow
Browse files Browse the repository at this point in the history
Fixes a recent regression in the login flow, and stops throwing
enterprise-related errors for non-enterprise projects when the user is
logged in.

Also added unit tests for the `login` command.
  • Loading branch information
thsig authored and eysi09 committed Aug 12, 2020
1 parent 34cfd8e commit 36865bc
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 31 deletions.
6 changes: 4 additions & 2 deletions garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { defaultDotIgnoreFiles } from "../util/fs"
import { renderError } from "../logger/renderers"
import { getDefaultProfiler } from "../util/profiling"
import { BufferedEventStream } from "../enterprise/buffered-event-stream"
import { makeEnterpriseContext } from "../enterprise/init"

export async function makeDummyGarden(root: string, gardenOpts: GardenOpts = {}) {
const environments = gardenOpts.environmentName
Expand Down Expand Up @@ -229,11 +230,12 @@ ${renderCommands(commands)}
garden = await Garden.factory(root, contextOpts)
}

if (garden.enterpriseContext) {
const enterpriseContext = makeEnterpriseContext(garden)
if (enterpriseContext) {
log.silly(`Connecting Garden instance to BufferedEventStream`)
bufferedEventStream.connect({
enterpriseContext,
eventBus: garden.events,
enterpriseContext: garden.enterpriseContext,
environmentName: garden.environmentName,
namespace: garden.namespace,
})
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class LoginCommand extends Command {

async action({ garden, log, headerLog }: CommandParams): Promise<CommandResult> {
printHeader(headerLog, "Login", "cloud")
const enterpriseDomain = garden.enterpriseContext?.enterpriseDomain
const enterpriseDomain = garden.enterpriseDomain
if (!enterpriseDomain) {
throw new ConfigurationError(`Error: Your project configuration does not specify a domain.`, {})
}
Expand Down
6 changes: 4 additions & 2 deletions garden-service/src/commands/run/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { registerWorkflowRun } from "../../enterprise/workflow-lifecycle"
import { parseCliArgs, pickCommand, processCliArgs } from "../../cli/helpers"
import { StringParameter } from "../../cli/params"
import { getAllCommands } from "../commands"
import { makeEnterpriseContext } from "../../enterprise/init"

const runWorkflowArgs = {
workflow: new StringParameter({
Expand Down Expand Up @@ -347,10 +348,11 @@ export function logErrors(
}

async function registerAndSetUid(garden: Garden, log: LogEntry, config: WorkflowConfig) {
if (garden.enterpriseContext && !gardenEnv.GARDEN_PLATFORM_SCHEDULED) {
const enterpriseContext = makeEnterpriseContext(garden)
if (enterpriseContext && !gardenEnv.GARDEN_PLATFORM_SCHEDULED) {
const workflowRunUid = await registerWorkflowRun({
enterpriseContext,
workflowConfig: config,
enterpriseContext: garden.enterpriseContext,
environment: garden.environmentName,
namespace: garden.namespace,
log,
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/enterprise/buffered-event-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { got } from "../util/http"
import { makeAuthHeader } from "./auth"
import { LogLevel } from "../logger/log-node"
import { gardenEnv } from "../constants"
import { GardenEnterpriseContext } from "../garden"
import { GardenEnterpriseContext } from "./init"

export type StreamEvent = {
name: EventName
Expand Down
43 changes: 38 additions & 5 deletions garden-service/src/enterprise/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ import { LogEntry } from "../logger/log-entry"
import { ProjectConfig } from "../config/project"
import { readAuthToken, checkClientAuthToken } from "./auth"
import { deline } from "../util/string"
import { ConfigurationError } from "../exceptions"
import { getSecrets } from "./secrets"
import { StringMap } from "../config/common"
import { Garden } from "../garden"

export interface EnterpriseInitParams {
log: LogEntry
projectConfig: ProjectConfig
environmentName: string
}

export interface GardenEnterpriseContext {
clientAuthToken: string
projectId: string
enterpriseDomain: string
}

export async function enterpriseInit({ log, projectConfig, environmentName }: EnterpriseInitParams) {
const { id: projectId, domain } = projectConfig
const clientAuthToken = await readAuthToken(log)
Expand All @@ -44,12 +50,11 @@ export async function enterpriseInit({ log, projectConfig, environmentName }: En
`)
}
if (errorMessages.length > 0) {
throw new ConfigurationError(
dedent`
log.verbose(
chalk.gray(dedent`
${errorMessages.join("\n\n")}
Logging out via the ${chalk.bold("garden logout")} command will suppress this message.`,
{}
Logging out via the ${chalk.bold("garden logout")} command will suppress this message.`)
)
}
} else {
Expand All @@ -75,3 +80,31 @@ export async function enterpriseInit({ log, projectConfig, environmentName }: En

return { clientAuthToken, secrets }
}

/**
* Returns null if one or more parameters are null.
*
* Returns a `GardenEnterpriseContext` otherwise.
*/
export function makeEnterpriseContext(garden: Garden): GardenEnterpriseContext | null {
const missing: string[] = []
if (!garden.clientAuthToken) {
missing.push("client auth token")
}
if (!garden.projectId) {
missing.push("project id")
}
if (!garden.enterpriseDomain) {
missing.push("domain")
}
if (missing.length > 0) {
garden.log.silly(`Enterprise features disabled. Missing values: ${missing.join(",")}`)
return null
} else {
return {
clientAuthToken: garden.clientAuthToken!,
projectId: garden.projectId!,
enterpriseDomain: garden.enterpriseDomain!,
}
}
}
2 changes: 1 addition & 1 deletion garden-service/src/enterprise/workflow-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { makeAuthHeader } from "./auth"
import { WorkflowConfig } from "../config/workflow"
import { LogEntry } from "../logger/log-entry"
import { PlatformError } from "../exceptions"
import { GardenEnterpriseContext } from "../garden"
import { GardenEnterpriseContext } from "./init"

export interface RegisterWorkflowRunParams {
workflowConfig: WorkflowConfig
Expand Down
32 changes: 13 additions & 19 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,12 @@ export interface GardenOpts {
variables?: PrimitiveMap
}

export interface GardenEnterpriseContext {
clientAuthToken: string
projectId: string
enterpriseDomain: string
}

export interface GardenParams {
artifactsPath: string
buildDir: BuildDir
clientAuthToken: string | null
enterpriseDomain: string | null
projectId: string | null
enterpriseContext: GardenEnterpriseContext | null
dotIgnoreFiles: string[]
environmentName: string
environmentConfigs: EnvironmentConfig[]
Expand Down Expand Up @@ -165,7 +160,8 @@ export class Garden {
private readonly taskGraph: TaskGraph
private watcher: Watcher
private asyncLock: any
public readonly enterpriseContext: GardenEnterpriseContext | null
public readonly clientAuthToken: string | null
public readonly enterpriseDomain: string | null
public readonly projectId: string | null
public sessionId: string | null
public readonly configStore: ConfigStore
Expand Down Expand Up @@ -202,7 +198,8 @@ export class Garden {

constructor(params: GardenParams) {
this.buildDir = params.buildDir
this.enterpriseContext = params.enterpriseContext
this.clientAuthToken = params.clientAuthToken
this.enterpriseDomain = params.enterpriseDomain
this.projectId = params.projectId
this.sessionId = params.sessionId
this.environmentName = params.environmentName
Expand Down Expand Up @@ -329,25 +326,22 @@ export class Garden {
await ensureConnected()

const sessionId = opts.sessionId || null

const { id: projectId, domain: enterpriseDomain } = config

const projectId = config.id || null
let secrets: StringMap = {}
let enterpriseContext: GardenEnterpriseContext | null = null
let clientAuthToken: string | null = null
const enterpriseDomain = config.domain || null
if (!opts.noEnterprise) {
const enterpriseInitResult = await enterpriseInit({ log, projectConfig: config, environmentName })
secrets = enterpriseInitResult.secrets
const clientAuthToken = enterpriseInitResult.clientAuthToken
if (clientAuthToken && projectId && enterpriseDomain) {
enterpriseContext = { clientAuthToken, projectId, enterpriseDomain }
}
clientAuthToken = enterpriseInitResult.clientAuthToken
}

const garden = new this({
artifactsPath,
sessionId,
projectId: projectId || null,
enterpriseContext,
clientAuthToken,
enterpriseDomain,
projectId,
projectRoot,
projectName,
environmentName,
Expand Down
4 changes: 4 additions & 0 deletions garden-service/test/unit/src/commands/get/get-outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ describe("GetOutputsCommand", () => {
}
})

after(async () => {
await tmpDir.cleanup()
})

it("should resolve and return defined project outputs", async () => {
const plugin = createGardenPlugin({
name: "test",
Expand Down
84 changes: 84 additions & 0 deletions garden-service/test/unit/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2018-2020 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { expect } from "chai"
import td from "testdouble"
import tmp from "tmp-promise"
import { ProjectConfig, defaultNamespace } from "../../../../src/config/project"
import { exec } from "../../../../src/util/util"
import { DEFAULT_API_VERSION } from "../../../../src/constants"
import { TestGarden, withDefaultGlobalOpts, expectError } from "../../../helpers"
const Auth = require("../../../../src/enterprise/auth")
import { LoginCommand } from "../../../../src/commands/login"
import stripAnsi from "strip-ansi"

function makeCommandParams(garden: TestGarden) {
const log = garden.log
return {
garden,
log,
headerLog: log,
footerLog: log,
args: {},
opts: withDefaultGlobalOpts({}),
}
}

describe("LoginCommand", () => {
let tmpDir: tmp.DirectoryResult
let projectConfig: ProjectConfig
const dummyDomain = "dummy-domain"

before(async () => {
tmpDir = await tmp.dir({ unsafeCleanup: true })
await exec("git", ["init"], { cwd: tmpDir.path })

projectConfig = {
apiVersion: DEFAULT_API_VERSION,
kind: "Project",
name: "test",
path: tmpDir.path,
defaultEnvironment: "default",
dotIgnoreFiles: [],
environments: [{ name: "default", defaultNamespace, variables: {} }],
providers: [{ name: "test" }],
variables: {},
}
})

beforeEach(async () => {
td.replace(Auth, "login", async () => "dummy-auth-token")
})

after(async () => {
await tmpDir.cleanup()
})

it("should log in if the project has a domain and an id", async () => {
const config = { ...projectConfig, domain: dummyDomain, id: "dummy-id" }
const garden = await TestGarden.factory(tmpDir.path, { config })
const command = new LoginCommand()
await command.action(makeCommandParams(garden))
})

it("should log in if the project has a domain but no id", async () => {
const config = { ...projectConfig, domain: dummyDomain }
const garden = await TestGarden.factory(tmpDir.path, { config })
const command = new LoginCommand()
await command.action(makeCommandParams(garden))
})

it("should throw if the project doesn't have a domain", async () => {
const garden = await TestGarden.factory(tmpDir.path, { config: projectConfig })
const command = new LoginCommand()
await expectError(
() => command.action(makeCommandParams(garden)),
(err) => expect(stripAnsi(err.message)).to.match(/Your project configuration does not specify a domain/)
)
})
})

0 comments on commit 36865bc

Please sign in to comment.