Skip to content

Commit

Permalink
feat: add login and logout commands
Browse files Browse the repository at this point in the history
  • Loading branch information
eysi09 committed Apr 25, 2018
1 parent 985c160 commit 00548e2
Show file tree
Hide file tree
Showing 29 changed files with 3,232 additions and 2,646 deletions.
5,107 changes: 2,608 additions & 2,499 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"fs-extra": "^5.0.0",
"has-ansi": "^3.0.0",
"ignore": "^3.3.7",
"inquirer": "^5.2.0",
"is-subset": "^0.1.1",
"joi": "^13.2.0",
"js-yaml": "^3.11.0",
Expand Down Expand Up @@ -63,6 +64,7 @@
"@types/fs-extra": "^5.0.2",
"@types/has-ansi": "^3.0.0",
"@types/joi": "^13.0.7",
"@types/inquirer": "0.0.41",
"@types/js-yaml": "^3.11.1",
"@types/klaw": "^2.1.1",
"@types/log-symbols": "^2.0.0",
Expand Down
5 changes: 4 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import { LogLevel } from "./logger/types"
import { ConfigCommand } from "./commands/config"
import { StatusCommand } from "./commands/status"
import { PushCommand } from "./commands/push"
import { enumToArray } from "./types/common"
import { LoginCommand } from "./commands/login"
import { LogoutCommand } from "./commands/logout"

const GLOBAL_OPTIONS = {
root: new StringParameter({
Expand Down Expand Up @@ -223,6 +224,8 @@ export class GardenCli {
new ValidateCommand(),
new StatusCommand(),
new PushCommand(),
new LoginCommand(),
new LogoutCommand(),
]
const globalOptions = Object.entries(GLOBAL_OPTIONS)

Expand Down
2 changes: 1 addition & 1 deletion src/commands/environment/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class EnvironmentDestroyCommand extends Command {
result = await ctx.destroyEnvironment()

if (!providersTerminated(result)) {
ctx.log.info("\nWaiting for providers to terminate")
ctx.log.info("Waiting for providers to terminate")
logEntries = reduce(result, (acc: LogEntryMap, status: EnvironmentStatus, provider: string) => {
if (status.configured) {
acc[provider] = ctx.log.info({
Expand Down
28 changes: 28 additions & 0 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* 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 { Command } from "./base"
import { EntryStyle } from "../logger/types"
import { PluginContext } from "../plugin-context"
import { LoginStatusMap } from "../types/plugin"

export class LoginCommand extends Command {
name = "login"
help = "Log into the Garden framework"

async action(ctx: PluginContext): Promise<LoginStatusMap> {
ctx.log.header({ emoji: "unlock", command: "Login" })
ctx.log.info({ msg: "Logging in...", entryStyle: EntryStyle.activity })

const result = await ctx.login()

ctx.log.info("\nLogin success!")

return result
}
}
30 changes: 30 additions & 0 deletions src/commands/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* 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 { Command } from "./base"
import { EntryStyle } from "../logger/types"
import { PluginContext } from "../plugin-context"
import { LoginStatusMap } from "../types/plugin"

export class LogoutCommand extends Command {
name = "logout"
help = "Log into the Garden framework"

async action(ctx: PluginContext): Promise<LoginStatusMap> {

ctx.log.header({ emoji: "lock", command: "Logout" })

const entry = ctx.log.info({ msg: "Logging out...", entryStyle: EntryStyle.activity })

const result = await ctx.logout()

entry.setSuccess("Logged out successfully")

return result
}
}
194 changes: 194 additions & 0 deletions src/config-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* 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 { resolve } from "path"
import { ensureFile, readFile } from "fs-extra"
import * as Joi from "joi"
import * as yaml from "js-yaml"
import { get, isPlainObject, unset } from "lodash"
import { joiIdentifier, Primitive, validate } from "./types/common"
import { LocalConfigError } from "./exceptions"
import { dumpYaml } from "./util"

export type ConfigValue = Primitive | Primitive[]

export type SetManyParam = { keyPath: Array<string>, value: ConfigValue }[]

export abstract class ConfigStore<T extends object = any> {
private cached: null | T
protected configPath: string

constructor(projectPath: string) {
this.configPath = this.setConfigPath(projectPath)
this.cached = null
}

abstract setConfigPath(projectPath: string): string
abstract validate(config): T

/**
* Would've been nice to allow something like: set(["path", "to", "valA", valA], ["path", "to", "valB", valB]...)
* but Typescript support is missing at the moment
*/
public async set(param: SetManyParam)
public async set(keyPath: string[], value: ConfigValue)
public async set(...args) {
let config = await this.getConfig()
let entries: SetManyParam

if (args.length === 1) {
entries = args[0]
} else {
entries = [{ keyPath: args[0], value: args[1] }]
}

for (const { keyPath, value } of entries) {
config = this.updateConfig(config, keyPath, value)
}

return this.saveLocalConfig(config)
}

public async get(): Promise<T>
public async get(keyPath: string[]): Promise<Object | ConfigValue>
public async get(keyPath?: string[]): Promise<Object | ConfigValue> {
const config = await this.getConfig()

if (keyPath) {
const value = get(config, keyPath)

if (value === undefined) {
this.throwKeyNotFound(config, keyPath)
}

return value
}

return config
}

public async clear() {
return this.saveLocalConfig(<T>{})
}

public async delete(keyPath: string[]) {
let config = await this.getConfig()
if (get(config, keyPath) === undefined) {
this.throwKeyNotFound(config, keyPath)
}
const success = unset(config, keyPath)
if (!success) {
throw new LocalConfigError(`Unable to delete key ${keyPath.join(".")} in user config`, {
keyPath,
config,
})
}
return this.saveLocalConfig(config)
}

private async getConfig(): Promise<T> {
let config: T
if (this.cached) {
// Spreading does not work on generic types, see: https://github.com/Microsoft/TypeScript/issues/13557
config = Object.assign(this.cached, {})
} else {
config = await this.loadConfig()
}
return config
}

private updateConfig(config: T, keyPath: string[], value: ConfigValue): T {
let currentValue = config

for (let i = 0; i < keyPath.length; i++) {
const k = keyPath[i]

if (i === keyPath.length - 1) {
currentValue[k] = value
} else if (currentValue[k] === undefined) {
currentValue[k] = {}
} else if (!isPlainObject(currentValue[k])) {
const path = keyPath.slice(i + 1).join(".")

throw new LocalConfigError(
`Attempting to assign a nested key on non-object (current value at ${path}: ${currentValue[k]})`,
{
currentValue: currentValue[k],
path,
},
)
}

currentValue = currentValue[k]
}
return config
}

private async ensureConfigFileExists() {
return ensureFile(this.configPath)
}

private async loadConfig(): Promise<T> {
await this.ensureConfigFileExists()
const config = await yaml.safeLoad((await readFile(this.configPath)).toString()) || {}

this.cached = this.validate(config)

return this.cached
}

private async saveLocalConfig(config: T) {
this.cached = null
const validated = this.validate(config)
await dumpYaml(this.configPath, validated)
this.cached = config
}

private throwKeyNotFound(config: T, keyPath: string[]) {
throw new LocalConfigError(`Could not find key ${keyPath.join(".")} in user config`, {
keyPath,
config,
})
}

}

export interface KubernetesLocalConfig {
username?: string
"previous-usernames"?: Array<string>
}

export interface LocalConfig {
kubernetes?: KubernetesLocalConfig
}

const kubernetesLocalConfigSchema = Joi.object().keys({
username: joiIdentifier().allow("").optional(),
"previous-usernames": Joi.array().items(joiIdentifier()).optional(),
})

// TODO: Dynamically populate schema with all possible provider keys?
const localConfigSchema = Joi.object().keys({
kubernetes: kubernetesLocalConfigSchema,
})

export class LocalConfigStore extends ConfigStore<LocalConfig> {

setConfigPath(projectPath): string {
return resolve(projectPath, ".garden", "local-config.yml")
}

validate(config): LocalConfig {
return validate(
config,
localConfigSchema,
{ context: this.configPath, ErrorClass: LocalConfigError },
)
}

}
8 changes: 8 additions & 0 deletions src/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ export abstract class GardenError extends Error {
}
}

export class AuthenticationError extends GardenError {
type = "authentication"
}

export class ConfigurationError extends GardenError {
type = "configuration"
}

export class LocalConfigError extends GardenError {
type = "local-config"
}

export class ValidationError extends GardenError {
type = "validation"
}
Expand Down
21 changes: 15 additions & 6 deletions src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ import {
loadConfig,
} from "./types/config"
import { Task } from "./types/task"
import {
LocalConfigStore,
} from "./config-store"

export interface ModuleMap<T extends Module> {
[key: string]: T
Expand Down Expand Up @@ -149,6 +152,7 @@ export class Garden {
private readonly environment: string,
private readonly namespace: string,
public readonly config: EnvironmentConfig,
public readonly localConfigStore: LocalConfigStore,
logger?: RootLogNode,
) {
this.modulesScanned = false
Expand All @@ -175,11 +179,12 @@ export class Garden {
}

static async factory(projectRoot: string, { env, config, logger, plugins = [] }: ContextOpts = {}) {
// const localConfig = new LocalConfig(projectRoot)
let parsedConfig: GardenConfig

const localConfigStore = new LocalConfigStore(projectRoot)

if (config) {
parsedConfig = <GardenConfig>validate(config, configSchema, "root configuration")
parsedConfig = <GardenConfig>validate(config, configSchema, { context: "root configuration" })

if (!parsedConfig.project) {
throw new ConfigurationError(`Supplied config does not contain a project configuration`, {
Expand Down Expand Up @@ -238,7 +243,7 @@ export class Garden {
// Resolve the project configuration based on selected environment
const projectConfig = merge({}, globalConfig, envConfig)

const garden = new Garden(projectRoot, projectName, environment, namespace, projectConfig, logger)
const garden = new Garden(projectRoot, projectName, environment, namespace, projectConfig, localConfigStore, logger)

// Register plugins
for (const plugin of builtinPlugins.concat(plugins)) {
Expand Down Expand Up @@ -307,7 +312,11 @@ export class Garden {
}

try {
pluginModule = validate(pluginModule, pluginModuleSchema, `plugin module "${moduleNameOrLocation}"`)
pluginModule = validate(
pluginModule,
pluginModuleSchema,
{ context: `plugin module "${moduleNameOrLocation}"` },
)

if (pluginModule.name) {
name = pluginModule.name
Expand All @@ -320,7 +329,7 @@ export class Garden {
}
}

validate(name, joiIdentifier(), `name of plugin "${moduleNameOrLocation}"`)
validate(name, joiIdentifier(), { context: `name of plugin "${moduleNameOrLocation}"` })
} catch (err) {
throw new PluginError(`Unable to load plugin: ${err}`, {
moduleNameOrLocation,
Expand Down Expand Up @@ -361,7 +370,7 @@ export class Garden {
})
}

plugin = validate(plugin, pluginSchema, `plugin "${pluginName}"`)
plugin = validate(plugin, pluginSchema, { context: `plugin "${pluginName}"` })

this.loadedPlugins[pluginName] = plugin

Expand Down
Loading

0 comments on commit 00548e2

Please sign in to comment.