Skip to content

Commit

Permalink
refactor: split provider from KubeApi
Browse files Browse the repository at this point in the history
This is to facilitate use of the KubeApi class e.g. in integration
tests, where some k8s operations need to be run outside of a Garden
project.
  • Loading branch information
thsig committed Mar 13, 2019
1 parent ee2829a commit 4787e51
Show file tree
Hide file tree
Showing 26 changed files with 272 additions and 184 deletions.
4 changes: 2 additions & 2 deletions garden-service/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const providerConfigBaseSchema = Joi.object()
.unknown(true)
.meta({ extendable: true })

export interface Provider<T extends ProviderConfig = any> {
export interface Provider<T extends ProviderConfig = ProviderConfig> {
name: string
config: T
}
Expand Down Expand Up @@ -151,5 +151,5 @@ export const projectSchema = Joi.object()
// this is used for default handlers in the action handler
export const defaultProvider: Provider = {
name: "_default",
config: {},
config: { name: "_default" },
}
5 changes: 3 additions & 2 deletions garden-service/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
projectSourcesSchema,
environmentSchema,
providerConfigBaseSchema,
ProviderConfig,
} from "./config/project"
import { joiIdentifier, joiIdentifierMap } from "./config/common"
import { PluginError } from "./exceptions"
Expand All @@ -37,8 +38,8 @@ const providerSchema = Joi.object()
config: providerConfigBaseSchema,
})

export interface PluginContext extends WrappedFromGarden {
provider: Provider
export interface PluginContext<C extends ProviderConfig = ProviderConfig> extends WrappedFromGarden {
provider: Provider<C>
providers: { [name: string]: Provider }
}

Expand Down
5 changes: 1 addition & 4 deletions garden-service/src/plugins/kubernetes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { safeLoad } from "js-yaml"
import { zip, omitBy, isObject } from "lodash"
import { GardenBaseError } from "../../exceptions"
import { homedir } from "os"
import { KubernetesProvider } from "./kubernetes"
import { KubernetesResource } from "./types"
import * as dedent from "dedent"

Expand Down Expand Up @@ -72,7 +71,6 @@ export class KubernetesError extends GardenBaseError {
}

export class KubeApi {
public context: string
private config: KubeConfig

public apiExtensions: Apiextensions_v1beta1Api
Expand All @@ -82,8 +80,7 @@ export class KubeApi {
public policy: Policy_v1beta1Api
public rbac: RbacAuthorization_v1Api

constructor(public provider: KubernetesProvider) {
this.context = provider.config.context
constructor(public context: string) {
this.config = getConfig(this.context)

for (const [name, cls] of Object.entries(apiTypes)) {
Expand Down
39 changes: 25 additions & 14 deletions garden-service/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import { getAppNamespace } from "../namespace"
import { PluginContext } from "../../../plugin-context"
import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../../constants"
import { KubeApi } from "../api"
import { KubernetesProvider } from "../kubernetes"
import { KubernetesProvider, KubernetesPluginContext } from "../kubernetes"
import { configureHotReload } from "../hot-reload"
import { KubernetesResource, KubeEnvVar } from "../types"
import { ConfigurationError } from "../../../exceptions"
import { getContainerServiceStatus } from "./status"
import { containerHelpers } from "../../container/helpers"
import { LogEntry } from "../../../logger/log-entry"

export const DEFAULT_CPU_REQUEST = "10m"
export const DEFAULT_CPU_LIMIT = "500m"
Expand All @@ -32,14 +33,21 @@ export const DEFAULT_MEMORY_LIMIT = "512Mi"

export async function deployContainerService(params: DeployServiceParams<ContainerModule>): Promise<ServiceStatus> {
const { ctx, service, runtimeContext, force, log, hotReload } = params
const k8sCtx = <KubernetesPluginContext>ctx

const namespace = await getAppNamespace(ctx, ctx.provider)
const objects = await createContainerObjects(ctx, service, runtimeContext, hotReload)
const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)
const objects = await createContainerObjects(k8sCtx, service, runtimeContext, hotReload)

// TODO: use Helm instead of kubectl apply
const pruneSelector = "service=" + service.name
await applyMany(ctx.provider.config.context, objects, { force, namespace, pruneSelector })
await waitForResources({ ctx, provider: ctx.provider, serviceName: service.name, resources: objects, log })
await applyMany(k8sCtx.provider.config.context, objects, { force, namespace, pruneSelector })
await waitForResources({
ctx: k8sCtx,
provider: k8sCtx.provider,
serviceName: service.name,
resources: objects,
log,
})

return getContainerServiceStatus(params)
}
Expand All @@ -50,11 +58,13 @@ export async function createContainerObjects(
runtimeContext: RuntimeContext,
enableHotReload: boolean,
) {
const k8sCtx = <KubernetesPluginContext>ctx
const version = service.module.version
const namespace = await getAppNamespace(ctx, ctx.provider)
const api = new KubeApi(ctx.provider)
const ingresses = await createIngressResources(api, namespace, service)
const deployment = await createDeployment(ctx.provider, service, runtimeContext, namespace, enableHotReload)
const provider = k8sCtx.provider
const namespace = await getAppNamespace(k8sCtx, provider)
const api = new KubeApi(provider.config.context)
const ingresses = await createIngressResources(api, provider, namespace, service)
const deployment = await createDeployment(provider, service, runtimeContext, namespace, enableHotReload)
const kubeservices = await createServiceResources(service, namespace)

const objects = [deployment, ...kubeservices, ...ingresses]
Expand Down Expand Up @@ -367,11 +377,12 @@ export function rsyncTargetPath(path: string) {

export async function deleteService(params: DeleteServiceParams): Promise<ServiceStatus> {
const { ctx, log, service } = params
const namespace = await getAppNamespace(ctx, ctx.provider)
const provider = ctx.provider
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)
const provider = k8sCtx.provider

const context = provider.config.context
await deleteContainerDeployment({ namespace, provider, serviceName: service.name, log })
await deleteContainerDeployment({ namespace, context, serviceName: service.name, log })
await deleteObjectsByLabel({
context,
namespace,
Expand All @@ -385,11 +396,11 @@ export async function deleteService(params: DeleteServiceParams): Promise<Servic
}

export async function deleteContainerDeployment(
{ namespace, provider, serviceName, log },
{ namespace, context, serviceName, log }: { namespace: string, context: string, serviceName: string, log: LogEntry },
) {

let found = true
const api = new KubeApi(provider)
const api = new KubeApi(context)

try {
await api.extensions.deleteNamespacedDeployment(serviceName, namespace, <any>{})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function configure(params: ConfigureModuleParams<ContainerModule>) {
const config = await configureContainerModule(params)

// validate ingress specs
const provider: KubernetesProvider = params.ctx.provider
const provider = <KubernetesProvider>params.ctx.provider

for (const serviceConfig of config.serviceConfigs) {
for (const ingressSpec of serviceConfig.spec.ingresses) {
Expand Down
38 changes: 22 additions & 16 deletions garden-service/src/plugins/kubernetes/container/ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { certpem } from "certpem"
import { find, extend } from "lodash"
import { findByName } from "../../../util/util"
import { ContainerService, ContainerIngressSpec } from "../../container/config"
import { IngressTlsCertificate } from "../kubernetes"
import { IngressTlsCertificate, KubernetesProvider } from "../kubernetes"
import { ServiceIngress, ServiceProtocol } from "../../../types/service"
import { KubeApi } from "../api"
import { ConfigurationError, PluginError } from "../../../exceptions"
Expand All @@ -24,12 +24,14 @@ interface ServiceIngressWithCert extends ServiceIngress {

const certificateHostnames: { [name: string]: string[] } = {}

export async function createIngressResources(api: KubeApi, namespace: string, service: ContainerService) {
export async function createIngressResources(
api: KubeApi, provider: KubernetesProvider, namespace: string, service: ContainerService,
) {
if (service.spec.ingresses.length === 0) {
return []
}

const allIngresses = await getIngressesWithCert(service, api)
const allIngresses = await getIngressesWithCert(service, api, provider)

return Bluebird.map(allIngresses, async (ingress) => {
const rules = [{
Expand All @@ -51,8 +53,8 @@ export async function createIngressResources(api: KubeApi, namespace: string, se
"ingress.kubernetes.io/force-ssl-redirect": !!cert + "",
}

if (api.provider.config.ingressClass) {
annotations["kubernetes.io/ingress.class"] = api.provider.config.ingressClass
if (provider.config.ingressClass) {
annotations["kubernetes.io/ingress.class"] = provider.config.ingressClass
}

extend(annotations, ingress.spec.annotations)
Expand Down Expand Up @@ -82,19 +84,19 @@ export async function createIngressResources(api: KubeApi, namespace: string, se
}

async function getIngress(
service: ContainerService, api: KubeApi, spec: ContainerIngressSpec,
service: ContainerService, api: KubeApi, provider: KubernetesProvider, spec: ContainerIngressSpec,
): Promise<ServiceIngressWithCert> {
const hostname = spec.hostname || api.provider.config.defaultHostname
const hostname = spec.hostname || provider.config.defaultHostname

if (!hostname) {
// this should be caught when parsing the module
throw new PluginError(`Missing hostname in ingress spec`, { serviceSpec: service.spec, ingressSpec: spec })
}

const certificate = await pickCertificate(service, api, hostname)
const certificate = await pickCertificate(service, api, provider, hostname)
// TODO: support other protocols
const protocol: ServiceProtocol = !!certificate ? "https" : "http"
const port = !!certificate ? api.provider.config.ingressHttpsPort : api.provider.config.ingressHttpPort
const port = !!certificate ? provider.config.ingressHttpsPort : provider.config.ingressHttpPort

return {
...spec,
Expand All @@ -107,12 +109,16 @@ async function getIngress(
}
}

async function getIngressesWithCert(service: ContainerService, api: KubeApi): Promise<ServiceIngressWithCert[]> {
return Bluebird.map(service.spec.ingresses, spec => getIngress(service, api, spec))
async function getIngressesWithCert(
service: ContainerService, api: KubeApi, provider: KubernetesProvider,
): Promise<ServiceIngressWithCert[]> {
return Bluebird.map(service.spec.ingresses, spec => getIngress(service, api, provider, spec))
}

export async function getIngresses(service: ContainerService, api: KubeApi): Promise<ServiceIngress[]> {
return (await getIngressesWithCert(service, api))
export async function getIngresses(
service: ContainerService, api: KubeApi, provider: KubernetesProvider,
): Promise<ServiceIngress[]> {
return (await getIngressesWithCert(service, api, provider))
.map(ingress => ({
hostname: ingress.hostname,
path: ingress.path,
Expand Down Expand Up @@ -188,9 +194,9 @@ async function getCertificateHostnames(api: KubeApi, cert: IngressTlsCertificate
}

async function pickCertificate(
service: ContainerService, api: KubeApi, hostname: string,
service: ContainerService, api: KubeApi, provider: KubernetesProvider, hostname: string,
): Promise<IngressTlsCertificate | undefined> {
for (const cert of api.provider.config.tlsCertificates) {
for (const cert of provider.config.tlsCertificates) {
const certHostnames = await getCertificateHostnames(api, cert)

for (const certHostname of certHostnames) {
Expand All @@ -203,7 +209,7 @@ async function pickCertificate(
}
}

if (api.provider.config.forceSsl) {
if (provider.config.forceSsl) {
throw new ConfigurationError(
`Could not find certificate for hostname '${hostname}' ` +
`configured on service '${service.name}' and forceSsl flag is set.`,
Expand Down
6 changes: 4 additions & 2 deletions garden-service/src/plugins/kubernetes/container/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { GetServiceLogsParams } from "../../../types/plugin/params"
import { ContainerModule } from "../../container/config"
import { getAppNamespace } from "../namespace"
import { getKubernetesLogs } from "../logs"
import { KubernetesPluginContext } from "../kubernetes"

export async function getServiceLogs(params: GetServiceLogsParams<ContainerModule>) {
const { ctx, service } = params
const context = ctx.provider.config.context
const namespace = await getAppNamespace(ctx, ctx.provider)
const k8sCtx = <KubernetesPluginContext>ctx
const context = k8sCtx.provider.config.context
const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)
const selector = `service=${service.name}`

return getKubernetesLogs({ ...params, context, namespace, selector })
Expand Down
12 changes: 8 additions & 4 deletions garden-service/src/plugins/kubernetes/container/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ import { kubectl } from "../kubectl"
import { getContainerServiceStatus } from "./status"
import { runPod } from "../run"
import { containerHelpers } from "../../container/helpers"
import { KubernetesPluginContext } from "../kubernetes"

export async function execInService(params: ExecInServiceParams<ContainerModule>) {
const { ctx, service, command, interactive } = params
const api = new KubeApi(ctx.provider)
const k8sCtx = <KubernetesPluginContext>ctx
const provider = k8sCtx.provider
const api = new KubeApi(provider.config.context)
const status = await getContainerServiceStatus({ ...params, hotReload: false })
const namespace = await getAppNamespace(ctx, ctx.provider)
const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)

// TODO: this check should probably live outside of the plugin
if (!includes(["ready", "outdated"], status.state)) {
Expand Down Expand Up @@ -78,8 +81,9 @@ export async function runContainerModule(
ctx, module, command, ignoreError = true, interactive, runtimeContext, timeout,
}: RunModuleParams<ContainerModule>,
): Promise<RunResult> {
const context = ctx.provider.config.context
const namespace = await getAppNamespace(ctx, ctx.provider)
const k8sCtx = <KubernetesPluginContext>ctx
const context = k8sCtx.provider.config.context
const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)
const image = await containerHelpers.getLocalImageId(module)

return runPod({
Expand Down
13 changes: 8 additions & 5 deletions garden-service/src/plugins/kubernetes/container/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ import { KubeApi } from "../api"
import { compareDeployedObjects } from "../status"
import { getIngresses } from "./ingress"
import { getAppNamespace } from "../namespace"
import { KubernetesPluginContext } from "../kubernetes"

export async function getContainerServiceStatus(
{ ctx, module, service, runtimeContext, log, hotReload }: GetServiceStatusParams<ContainerModule>,
): Promise<ServiceStatus> {

const k8sCtx = <KubernetesPluginContext>ctx
// TODO: hash and compare all the configuration files (otherwise internal changes don't get deployed)
const version = module.version
const api = new KubeApi(ctx.provider)
const namespace = await getAppNamespace(ctx, ctx.provider)
const provider = k8sCtx.provider
const api = new KubeApi(provider.config.context)
const namespace = await getAppNamespace(k8sCtx, provider)

// FIXME: [objects, matched] and ingresses can be run in parallel
const objects = await createContainerObjects(ctx, service, runtimeContext, hotReload)
const { state, remoteObjects } = await compareDeployedObjects(ctx, api, namespace, objects, log)
const ingresses = await getIngresses(service, api)
const objects = await createContainerObjects(k8sCtx, service, runtimeContext, hotReload)
const { state, remoteObjects } = await compareDeployedObjects(k8sCtx, api, namespace, objects, log)
const ingresses = await getIngresses(service, api, provider)

return {
ingresses,
Expand Down
4 changes: 3 additions & 1 deletion garden-service/src/plugins/kubernetes/helm/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { dumpYaml } from "../../../util/util"
import { LogEntry } from "../../../logger/log-entry"
import { getNamespace } from "../namespace"
import { apply as jsonMerge } from "json-merge-patch"
import { KubernetesPluginContext } from "../kubernetes"

export async function buildHelmModule({ ctx, module, log }: BuildModuleParams<HelmModule>): Promise<BuildResult> {
const namespace = await getNamespace({ ctx, provider: ctx.provider, skipCreate: true })
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({ ctx: k8sCtx, provider: k8sCtx.provider, skipCreate: true })
const context = ctx.provider.config.context
const baseModule = getBaseModule(module)

Expand Down
7 changes: 5 additions & 2 deletions garden-service/src/plugins/kubernetes/helm/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Module } from "../../../types/module"
import { findByName } from "../../../util/util"
import { deline } from "../../../util/string"
import { getAnnotation } from "../util"
import { KubernetesPluginContext } from "../kubernetes"

/**
* Returns true if the specified Helm module contains a template (as opposed to just referencing a remote template).
Expand All @@ -38,9 +39,10 @@ export async function containsSource(config: HelmModuleConfig) {
* Render the template in the specified Helm module (locally), and return all the resources in the chart.
*/
export async function getChartResources(ctx: PluginContext, module: Module, log: LogEntry) {
const k8sCtx = <KubernetesPluginContext>ctx
const chartPath = await getChartPath(module)
const valuesPath = getValuesPath(chartPath)
const namespace = await getNamespace({ ctx, provider: ctx.provider, skipCreate: true })
const namespace = await getNamespace({ ctx: k8sCtx, provider: k8sCtx.provider, skipCreate: true })
const context = ctx.provider.config.context
const releaseName = getReleaseName(module)

Expand Down Expand Up @@ -266,7 +268,8 @@ async function renderHelmTemplateString(
): Promise<string> {
const tempFilePath = join(chartPath, "templates", cryptoRandomString(16))
const valuesPath = getValuesPath(chartPath)
const namespace = await getNamespace({ ctx, provider: ctx.provider, skipCreate: true })
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({ ctx: k8sCtx, provider: k8sCtx.provider, skipCreate: true })
const releaseName = getReleaseName(module)
const context = ctx.provider.config.context

Expand Down
Loading

0 comments on commit 4787e51

Please sign in to comment.