Skip to content

Commit

Permalink
Merge pull request #82 from garden-io/login-command
Browse files Browse the repository at this point in the history
Login command
  • Loading branch information
eysi09 authored Apr 25, 2018
2 parents c795235 + 00548e2 commit 7ce80de
Show file tree
Hide file tree
Showing 32 changed files with 3,298 additions and 2,689 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
30 changes: 14 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@

import * as sywac from "sywac"
import chalk from "chalk"
import { shutdown } from "./util"
import { enumToArray, shutdown } from "./util"
import { merge, intersection, reduce } from "lodash"
import {
BooleanParameter,
Command,
ChoicesParameter,
ParameterValues,
Expand All @@ -36,6 +35,8 @@ import { LogLevel } from "./logger/types"
import { ConfigCommand } from "./commands/config"
import { StatusCommand } from "./commands/status"
import { PushCommand } from "./commands/push"
import { LoginCommand } from "./commands/login"
import { LogoutCommand } from "./commands/logout"

const GLOBAL_OPTIONS = {
root: new StringParameter({
Expand All @@ -44,14 +45,11 @@ const GLOBAL_OPTIONS = {
defaultValue: process.cwd(),
}),
env: new EnvironmentOption(),
verbose: new BooleanParameter({
alias: "v",
help: "verbose logging",
overrides: ["silent"],
}),
silent: new BooleanParameter({
alias: "s",
help: "silence logger",
loglevel: new ChoicesParameter({
alias: "log",
choices: enumToArray(LogLevel),
help: "set logger level",
defaultValue: LogLevel[LogLevel.info],
}),
}
const GLOBAL_OPTIONS_GROUP_NAME = "Global options"
Expand Down Expand Up @@ -226,6 +224,8 @@ export class GardenCli {
new ValidateCommand(),
new StatusCommand(),
new PushCommand(),
new LoginCommand(),
new LogoutCommand(),
]
const globalOptions = Object.entries(GLOBAL_OPTIONS)

Expand Down Expand Up @@ -262,19 +262,17 @@ export class GardenCli {

const action = async argv => {
const logger = this.logger

// Sywac returns positional args and options in a single object which we separate into args and opts
const argsForAction = filterByArray(argv, argKeys)
const optsForAction = filterByArray(argv, optKeys.concat(globalKeys))
const root = resolve(process.cwd(), optsForAction.root)
const env = optsForAction.env

// Update logger config
if (argv.silent) {
logger.level = LogLevel.silent
logger.writers = []
} else {
const level = argv.verbose ? LogLevel.verbose : logger.level
logger.level = level
const level = LogLevel[<string>argv.loglevel]
logger.level = level
if (level !== LogLevel.silent) {
logger.writers.push(
new FileWriter({ level, root }),
new FileWriter({ level: LogLevel.error, filename: ERROR_LOG_FILENAME, root }),
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. <[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 { 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. <[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 { 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. <[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 { 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
Loading

0 comments on commit 7ce80de

Please sign in to comment.