Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ephemeral kubernetes provider #4927

Merged
merged 42 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7873cd6
chore: wip
shumailxyz Jul 27, 2023
4a8f93b
chore: wip2
shumailxyz Jul 31, 2023
10dd70e
chore: wip3
shumailxyz Aug 3, 2023
41556da
chore: improve logs
shumailxyz Aug 8, 2023
43408e6
chore: remove dockerhub imagepull secret
shumailxyz Aug 17, 2023
ebcf2c8
chore: add ephemeral ingresses mvp
shumailxyz Aug 17, 2023
51f44ee
chore: add service ports annotations
shumailxyz Aug 24, 2023
52c2c4e
chore: add cluster deadline and api response types
shumailxyz Aug 25, 2023
ea2c1f3
fix: lint
shumailxyz Aug 25, 2023
02d1829
chore: wip
shumailxyz Aug 29, 2023
6df392d
chore: add ephemeral ingress chart
shumailxyz Aug 30, 2023
2acfcdc
chore: update secret name
shumailxyz Aug 31, 2023
44d3c15
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 5, 2023
d04633c
refactor: remove unnecessary config and use api types from pkg
shumailxyz Sep 7, 2023
4f3a8b8
chore: fix ephemeral-nginx config
shumailxyz Sep 7, 2023
0e48cd7
chore: remove old changes that are not needed anymore
shumailxyz Sep 7, 2023
21aaf9b
docs: add docs for ephemeral-clusters
shumailxyz Sep 7, 2023
1b6bbf2
chore: update error class
shumailxyz Sep 7, 2023
3144537
chore: fix lint
shumailxyz Sep 7, 2023
498f2ac
fix: lint
shumailxyz Sep 7, 2023
bdb043d
test: add some tests for provider
shumailxyz Sep 7, 2023
bb9dac3
docs: add an example project using ephemeral-kubernetes provider
shumailxyz Sep 7, 2023
bc3d723
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 7, 2023
e70aadf
chore: change error type to configuration error
shumailxyz Sep 7, 2023
cb893c9
docs: link example in docs
shumailxyz Sep 8, 2023
e68e340
chore: show message returned from the api in case of api error
shumailxyz Sep 8, 2023
6a004c1
docs: typo
shumailxyz Sep 8, 2023
57d10b3
chore: set protocol as https for ephemeral cluster ingresses
shumailxyz Sep 8, 2023
5033131
docs: update docs
shumailxyz Sep 8, 2023
1b50e43
docs: update sidebar title
shumailxyz Sep 8, 2023
7da8f33
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 12, 2023
0dcca64
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 12, 2023
65193c0
chore: update docs
shumailxyz Sep 12, 2023
790a442
chore: fix lint
shumailxyz Sep 12, 2023
6ed6830
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 12, 2023
4090b95
chore: address comments
shumailxyz Sep 13, 2023
e38be3b
Merge remote-tracking branch 'origin/main' into feat-ephemeral-cluster
shumailxyz Sep 13, 2023
0b0945b
chore: rebase and regenerate docs
shumailxyz Sep 13, 2023
5c9d4ad
chore: fix lint
shumailxyz Sep 13, 2023
77898bf
chore: remove unused imports
shumailxyz Sep 13, 2023
19810d4
chore: remove link temporarily to avoid dead link
shumailxyz Sep 13, 2023
8fbbbed
chore: update docs
shumailxyz Sep 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
"devDependencies": {
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@garden-io/platform-api-types": "1.455.0",
"@garden-io/platform-api-types": "1.914.0",
"@google-cloud/kms": "^3.7.0",
"@types/analytics-node": "^3.1.10",
"@types/async": "^3.2.18",
Expand Down
37 changes: 34 additions & 3 deletions core/src/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import { Cookie } from "tough-cookie"
import { cloneDeep, isObject } from "lodash"
import { dedent, deline } from "../util/string"
import {
GetProjectResponse,
GetProfileResponse,
BaseResponse,
CreateEphemeralClusterResponse,
CreateProjectsForRepoResponse,
EphemeralClusterWithRegistry,
GetKubeconfigResponse,
GetProfileResponse,
GetProjectResponse,
ListProjectsResponse,
BaseResponse,
} from "@garden-io/platform-api-types"
import { getCloudDistributionName, getCloudLogSectionName, getPackageVersion } from "../util/util"
import { CommandInfo } from "../plugin-context"
Expand All @@ -36,6 +39,10 @@ const gardenClientVersion = getPackageVersion()
export class CloudApiDuplicateProjectsError extends CloudApiError {}
export class CloudApiTokenRefreshError extends CloudApiError {}

function extractErrorMessageBodyFromGotError(error: any): error is GotHttpError {
return error?.response?.body?.message
}

function stripLeadingSlash(str: string) {
return str.replace(/^\/+/, "")
}
Expand Down Expand Up @@ -786,4 +793,28 @@ export class CloudApi {
}
return secrets
}

async createEphemeralCluster(): Promise<EphemeralClusterWithRegistry> {
try {
const response = await this.post<CreateEphemeralClusterResponse>(`/ephemeral-clusters/`)
return response.data
} catch (err) {
throw new CloudApiError({
message: `${extractErrorMessageBodyFromGotError(err) ?? "Creating an ephemeral cluster failed."}`,
})
}
}

async getKubeConfigForCluster(clusterId: string): Promise<string> {
try {
const response = await this.get<GetKubeconfigResponse>(`/ephemeral-clusters/${clusterId}/kubeconfig`)
return response.data.kubeconfig
} catch (err) {
throw new CloudApiError({
message: `${
extractErrorMessageBodyFromGotError(err) ?? "Fetching the Kubeconfig for ephemeral cluster failed."
}`,
})
}
}
}
11 changes: 9 additions & 2 deletions core/src/plugins/kubernetes/container/ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { V1Ingress, V1Secret } from "@kubernetes/client-node"
import { Log } from "../../../logger/log-entry"
import chalk from "chalk"
import { Resolved } from "../../../actions/types"
import { isProviderEphemeralKubernetes } from "../ephemeral/ephemeral"

// Ingress API versions in descending order of preference
export const supportedIngressApiVersions = ["networking.k8s.io/v1", "networking.k8s.io/v1beta1", "extensions/v1beta1"]
Expand Down Expand Up @@ -183,8 +184,14 @@ async function getIngress(

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

// ephemeral-kubernetes ingresses should always be https
if (isProviderEphemeralKubernetes(provider)) {
protocol = "https"
port = provider.config.ingressHttpsPort
}

return {
...spec,
Expand Down
137 changes: 137 additions & 0 deletions core/src/plugins/kubernetes/ephemeral/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2018-2023 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 chalk from "chalk"
import { mkdirp, writeFile } from "fs-extra"
import { load } from "js-yaml"
import { remove } from "lodash"
import moment from "moment"
import { join } from "path"
import { joi, joiProviderName } from "../../../config/common"
import { providerConfigBaseSchema } from "../../../config/provider"
import { ConfigurationError } from "../../../exceptions"
import { ConfigureProviderParams } from "../../../plugin/handlers/Provider/configureProvider"
import { dedent } from "../../../util/string"
import { KubernetesConfig, namespaceSchema } from "../config"
import { EPHEMERAL_KUBERNETES_PROVIDER_NAME } from "./ephemeral"

export const configSchema = () =>
providerConfigBaseSchema()
.keys({
name: joiProviderName(EPHEMERAL_KUBERNETES_PROVIDER_NAME),
namespace: namespaceSchema().description(
"Specify which namespace to deploy services to (defaults to the project name). " +
"Note that the framework generates other namespaces as well with this name as a prefix."
),
setupIngressController: joi
.string()
.allow("nginx", false, null)
.default("nginx")
.description(
dedent`Set this to null or false to skip installing/enabling the \`nginx\` ingress controller. Note: if you skip installing the \`nginx\` ingress controller for ephemeral cluster, your ingresses may not function properly.`
),
})
.description(`The provider configuration for the ${EPHEMERAL_KUBERNETES_PROVIDER_NAME} plugin.`)

export async function configureProvider(params: ConfigureProviderParams<KubernetesConfig>) {
const { base, log, projectName, ctx, config: baseConfig } = params
if (projectName === "garden-system") {
// avoid configuring ephemeral-kubernetes provider and creating ephemeral-cluster for garden-system project
return {
config: baseConfig,
}
}
log.info(`Configuring ${EPHEMERAL_KUBERNETES_PROVIDER_NAME} provider for project ${projectName}`)
if (!ctx.cloudApi) {
throw new ConfigurationError({
message: `You are not logged in. You must be logged into Garden Cloud in order to use ${EPHEMERAL_KUBERNETES_PROVIDER_NAME} provider.`,
})
}
if (ctx.cloudApi && ctx.cloudApi?.domain !== "https://app.garden.io") {
throw new ConfigurationError({
message: `${EPHEMERAL_KUBERNETES_PROVIDER_NAME} provider is currently not supported for ${ctx.cloudApi.distroName}.`,
})
}
// creating tmp dir .garden/ephemeral-kubernetes for storing kubeconfig
const ephemeralClusterDirPath = join(ctx.gardenDirPath, "ephemeral-kubernetes")
await mkdirp(ephemeralClusterDirPath)
log.info("Creating ephemeral kubernetes cluster")
const createEphemeralClusterResponse = await ctx.cloudApi.createEphemeralCluster()
const clusterId = createEphemeralClusterResponse.instanceMetadata.instanceId
log.info(`Ephemeral kubernetes cluster created successfully`)
const deadlineDateTime = moment(createEphemeralClusterResponse.instanceMetadata.deadline)
const diffInNowAndDeadline = moment.duration(deadlineDateTime.diff(moment())).asMinutes().toFixed(1)
log.info(
chalk.white(
`Ephemeral cluster will be destroyed in ${diffInNowAndDeadline} minutes, at ${deadlineDateTime.format(
"YYYY-MM-DD HH:mm:ss"
)}`
)
)
log.info("Getting Kubeconfig for the cluster")
const kubeConfig = await ctx.cloudApi.getKubeConfigForCluster(clusterId)
const kubeconfigFileName = `${clusterId}-kubeconfig.yaml`
const kubeConfigPath = join(ctx.gardenDirPath, "ephemeral-kubernetes", kubeconfigFileName)
await writeFile(kubeConfigPath, kubeConfig)
log.info(`Kubeconfig for ephemeral cluster saved at path: ${chalk.underline(kubeConfigPath)}`)

const parsedKubeConfig: any = load(kubeConfig)
const currentContext = parsedKubeConfig["current-context"]
baseConfig.context = currentContext
baseConfig.kubeconfig = kubeConfigPath

// set deployment registry
baseConfig.deploymentRegistry = {
hostname: createEphemeralClusterResponse.registry.endpointAddress,
namespace: createEphemeralClusterResponse.registry.repository,
insecure: false,
}
// set imagePullSecrets
baseConfig.imagePullSecrets = [
{
name: createEphemeralClusterResponse.registry.imagePullSecret.name,
namespace: createEphemeralClusterResponse.registry.imagePullSecret.namespace,
},
]
// set build mode to kaniko
baseConfig.buildMode = "kaniko"
// set additional kaniko flags
baseConfig.kaniko = {
extraFlags: [
`--registry-mirror=${createEphemeralClusterResponse.registry.endpointAddress}`,
`--registry-mirror=${createEphemeralClusterResponse.registry.dockerRegistryMirror}`,
"--insecure-pull",
"--force",
],
}
// set setupIngressController to null while initializing kubernetes plugin
// as we use it later and configure it separately for ephemeral-kubernetes
const kubernetesPluginConfig = {
...params,
config: {
...baseConfig,
setupIngressController: null,
},
}
let { config: updatedConfig } = await base!(kubernetesPluginConfig)

// setup ingress controller unless setupIngressController is set to false/null in provider config
if (baseConfig.setupIngressController) {
const _systemServices = updatedConfig._systemServices
const nginxServices = ["ingress-controller", "default-backend"]
remove(_systemServices, (s) => nginxServices.includes(s))
_systemServices.push("nginx-ephemeral")
updatedConfig.setupIngressController = "nginx"
// set default hostname
updatedConfig.defaultHostname = createEphemeralClusterResponse.ingressesHostname
}

return {
config: updatedConfig,
}
}
45 changes: 45 additions & 0 deletions core/src/plugins/kubernetes/ephemeral/ephemeral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2023 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 { configureProvider, configSchema } from "./config"
import { createGardenPlugin } from "../../../plugin/plugin"
import { dedent } from "../../../util/string"
import { KubernetesProvider } from "../config"
import { joi, joiIdentifier } from "../../../config/common"

const providerUrl = "./kubernetes.md"
export const EPHEMERAL_KUBERNETES_PROVIDER_NAME = "ephemeral-kubernetes"

const outputsSchema = joi.object().keys({
"app-namespace": joiIdentifier().required().description("The primary namespace used for resource deployments."),
"default-hostname": joi
.string()
.description(
"The dynamic hostname assigned to the ephemeral cluster automatically, when an ephemeral cluster is created."
),
})

export const gardenPlugin = () =>
createGardenPlugin({
name: EPHEMERAL_KUBERNETES_PROVIDER_NAME,
base: "kubernetes",
docs: dedent`
The \`${EPHEMERAL_KUBERNETES_PROVIDER_NAME}\` provider is a specialized version of the [\`kubernetes\` provider](${providerUrl}) that allows to deploy applications to one of the ephemeral Kubernetes clusters provided by Garden.

For information about using ephemeral Kubernetes clusters, please refer to [Ephemeral Kubernetes clusters guide](../../basics/ephemeral-clusters.md)
`,
configSchema: configSchema(),
outputsSchema,
handlers: {
configureProvider,
},
})

export function isProviderEphemeralKubernetes(provider: KubernetesProvider) {
return provider?.name === EPHEMERAL_KUBERNETES_PROVIDER_NAME
}
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const helmDeploy: DeployActionHandler<"deploy", HelmDeployAction> = async
attached = true
}
// Get ingresses of deployed resources
const ingresses = getK8sIngresses(manifests)
const ingresses = getK8sIngresses(manifests, provider)

return {
state: "ready",
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/helm/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const getHelmDeployStatus: DeployActionHandler<"getStatus", HelmDeployAct
const deployedResources = await getDeployedChartResources({ ctx: k8sCtx, action, releaseName, log })

forwardablePorts = getForwardablePorts({ resources: deployedResources, parentAction: action, mode: deployedMode })
ingresses = getK8sIngresses(deployedResources)
ingresses = getK8sIngresses(deployedResources, provider)

if (state === "ready") {
// Local mode always takes precedence over sync mode
Expand Down
7 changes: 4 additions & 3 deletions core/src/plugins/kubernetes/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { mapValues, omit } from "lodash"
import { getIngressApiVersion, supportedIngressApiVersions } from "./container/ingress"
import { Log } from "../../logger/log-entry"
import { DeployStatusMap } from "../../plugin/handlers/Deploy/get-status"
import { isProviderEphemeralKubernetes } from "./ephemeral/ephemeral"

const dockerAuthSecretType = "kubernetes.io/dockerconfigjson"
const dockerAuthDocsLink = `
Expand Down Expand Up @@ -230,9 +231,9 @@ export async function prepareSystem({
return {}
}

// We require manual init if we're installing any system services to remote clusters, to avoid conflicts
// between users or unnecessary work.
if (!clusterInit && remoteCluster) {
// We require manual init if we're installing any system services to remote clusters unless the remote cluster
// is an ephemeral-cluster, to avoid conflicts between users or unnecessary work.
if (!clusterInit && remoteCluster && !isProviderEphemeralKubernetes(provider)) {
const initCommand = chalk.white.bold(`garden --env=${ctx.environmentName} plugins kubernetes cluster-init`)

if (combinedState === "ready") {
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/kubernetes-type/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export const getKubernetesDeployStatus: DeployActionHandler<"getStatus", Kuberne
version: state === "ready" ? action.versionString() : undefined,
detail: { remoteResources },
mode: deployedMode,
ingresses: getK8sIngresses(remoteResources),
ingresses: getK8sIngresses(remoteResources, provider),
},
// TODO-0.13.1
outputs: {},
Expand Down
14 changes: 11 additions & 3 deletions core/src/plugins/kubernetes/status/ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { ServiceIngress } from "../../../types/service"
import { ServiceIngress, ServiceProtocol } from "../../../types/service"
import { KubernetesProvider } from "../config"
import { isProviderEphemeralKubernetes } from "../ephemeral/ephemeral"
import { KubernetesIngress, KubernetesResource } from "../types"

/**
* Returns a list of ServiceIngresses found in a list of k8s resources.
*
* Does a best-effort extraction based on known ingress resource types.
*/
export function getK8sIngresses(resources: KubernetesResource[]): ServiceIngress[] {
export function getK8sIngresses(resources: KubernetesResource[], provider?: KubernetesProvider): ServiceIngress[] {
const output: ServiceIngress[] = []

for (const r of resources.filter(isIngressResource)) {
Expand All @@ -40,9 +42,15 @@ export function getK8sIngresses(resources: KubernetesResource[]): ServiceIngress
stringPath = path
}

let protocol: ServiceProtocol = tlsHosts.includes(rule.host) ? "https" : "http"
// ephemeral-kubernetes ingresses should always be https
if (provider && isProviderEphemeralKubernetes(provider)) {
protocol = "https"
}

output.push({
hostname: rule.host,
protocol: tlsHosts.includes(rule.host) ? "https" : "http",
protocol,
path: stringPath,
})
}
Expand Down
1 change: 1 addition & 0 deletions core/src/plugins/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getSupportedPlugins = () => [
{ name: "hadolint", callback: () => require("./hadolint/hadolint").gardenPlugin.getSpec() },
{ name: "kubernetes", callback: () => require("./kubernetes/kubernetes").gardenPlugin() },
{ name: "local-kubernetes", callback: () => require("./kubernetes/local/local").gardenPlugin() },
{ name: "ephemeral-kubernetes", callback: () => require("./kubernetes/ephemeral/ephemeral").gardenPlugin() },
{ name: "openshift", callback: () => require("./openshift/openshift").gardenPlugin() },
{ name: "octant", callback: () => require("./octant/octant").gardenPlugin() },
{ name: "otel-collector", callback: () => require("./otel-collector/otel-collector").gardenPlugin.getSpec() },
Expand Down
2 changes: 2 additions & 0 deletions core/test/helpers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class FakeCloudApi extends CloudApi {
cachedPermissions: {},
accessTokens: [],
groups: [],
meta: {},
singleProjectId: "",
}
}

Expand Down