Skip to content

Commit

Permalink
improvement: allow setting cred helpers in ImagePullSecrets
Browse files Browse the repository at this point in the history
This enables setting credHelper key for docker builders (kaniko/dd)
This means we can use docker repositories which have rolling
credentials such as ECR. We run this on ECR + kaniko
  • Loading branch information
swist authored and edvald committed Mar 10, 2020
1 parent 59230e9 commit b293fe2
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 52 deletions.
109 changes: 61 additions & 48 deletions garden-service/src/plugins/kubernetes/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getMetadataNamespace,
getSystemNamespace,
} from "./namespace"
import { KubernetesPluginContext, KubernetesConfig, KubernetesProvider } from "./config"
import { KubernetesPluginContext, KubernetesConfig, KubernetesProvider, ProviderSecretRef } from "./config"
import { checkTillerStatus, migrateToHelm3 } from "./helm/tiller"
import { prepareSystemServices, getSystemServiceStatus, getSystemGarden, systemNamespaceUpToDate } from "./system"
import { GetEnvironmentStatusParams, EnvironmentStatus } from "../../types/plugin/provider/getEnvironmentStatus"
Expand All @@ -33,7 +33,6 @@ import {
import { ConfigurationError } from "../../exceptions"
import Bluebird from "bluebird"
import { readSecret } from "./secrets"
import { extend } from "lodash"
import { dockerAuthSecretName, dockerAuthSecretKey } from "./constants"
import { V1Secret } from "@kubernetes/client-node"
import { KubernetesResource } from "./types"
Expand Down Expand Up @@ -465,76 +464,90 @@ export function getRegistryHostname(config: KubernetesConfig) {
const systemNamespace = config.gardenSystemNamespace
return `garden-docker-registry.${systemNamespace}.svc.cluster.local`
}
async function prepareDockerAuth(
api: KubeApi,
provider: KubernetesProvider,
log: LogEntry
): Promise<KubernetesResource<V1Secret>> {
// Read all configured imagePullSecrets and combine into a docker config file to use in the in-cluster builders.
const auths: { [name: string]: any } = {}

await Bluebird.map(provider.config.imagePullSecrets, async (secretRef) => {
const secret = await readSecret(api, secretRef)

if (secret.type !== dockerAuthSecretType) {
throw new ConfigurationError(
dedent`
interface DockerConfigJson {
experimental: string
auths: { [registry: string]: { [key: string]: string } }
credHelpers: { [registry: string]: any }
}
export async function buildDockerAuthConfig(
imagePullSecrets: ProviderSecretRef[],
api: KubeApi
): Promise<DockerConfigJson> {
return Bluebird.reduce(
imagePullSecrets,
async (accumulator, secretRef) => {
const secret = await readSecret(api, secretRef)
if (secret.type !== dockerAuthSecretType) {
throw new ConfigurationError(
dedent`
Configured imagePullSecret '${secret.metadata.name}' does not appear to be a valid registry secret, because
it does not have \`type: ${dockerAuthSecretType}\`.
${dockerAuthDocsLink}
`,
{ secretRef }
)
}
{ secretRef }
)
}

// Decode the secret
const encoded = secret.data && secret.data![dockerAuthSecretKey]
// Decode the secret
const encoded = secret.data && secret.data![dockerAuthSecretKey]

if (!encoded) {
throw new ConfigurationError(
dedent`
if (!encoded) {
throw new ConfigurationError(
dedent`
Configured imagePullSecret '${secret.metadata.name}' does not appear to be a valid registry secret, because
it does not contain a ${dockerAuthSecretKey} key.
${dockerAuthDocsLink}
`,
{ secretRef }
)
}
{ secretRef }
)
}

let decoded: any
let decoded: any

try {
decoded = JSON.parse(Buffer.from(encoded, "base64").toString())
} catch (err) {
throw new ConfigurationError(
dedent`
try {
decoded = JSON.parse(Buffer.from(encoded, "base64").toString())
} catch (err) {
throw new ConfigurationError(
dedent`
Could not parse configured imagePullSecret '${secret.metadata.name}' as a JSON docker authentication file:
${err.message}.
${dockerAuthDocsLink}
`,
{ secretRef }
)
}

if (!decoded.auths) {
throw new ConfigurationError(
dedent`
{ secretRef }
)
}
if (!decoded.auths && !decoded.credHelpers) {
throw new ConfigurationError(
dedent`
Could not parse configured imagePullSecret '${secret.metadata.name}' as a valid docker authentication file,
because it is missing an "auths" key.
because it is missing an "auths", "credHelpers" key.
${dockerAuthDocsLink}
`,
{ secretRef }
)
}
{ secretRef }
)
}
return {
...accumulator,
auths: { ...accumulator.auths, ...decoded.auths },
credHelpers: { ...accumulator.credHelpers, ...decoded.credHelpers },
}
},
{ experimental: "enabled", auths: {}, credHelpers: {} }
)
}

extend(auths, decoded.auths)
})
export async function prepareDockerAuth(
api: KubeApi,
provider: KubernetesProvider,
log: LogEntry
): Promise<KubernetesResource<V1Secret>> {
// Read all configured imagePullSecrets and combine into a docker config file to use in the in-cluster builders.
const config = await buildDockerAuthConfig(provider.config.imagePullSecrets, api)

// Enabling experimental features, in order to support advanced registry querying
const config = { auths, experimental: "enabled" }

// Store the config as a Secret (overwriting if necessary)
const systemNamespace = await getSystemNamespace(provider, log)
const systemNamespace = await getSystemNamespace(provider, log, api)

return {
apiVersion: "v1",
Expand Down
7 changes: 4 additions & 3 deletions garden-service/src/plugins/kubernetes/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ export async function getNamespace({
return namespace
}

export async function getSystemNamespace(provider: KubernetesProvider, log: LogEntry): Promise<string> {
export async function getSystemNamespace(provider: KubernetesProvider, log: LogEntry, api?: KubeApi): Promise<string> {
const namespace = provider.config.gardenSystemNamespace

const api = await KubeApi.factory(log, provider)
if (!api) {
api = await KubeApi.factory(log, provider)
}
await ensureNamespace(api, namespace)

return namespace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ providers:
imagePullSecrets:
# Note: We populate this secret in the test code
- name: test-docker-auth
- name: test-cred-helper-auth
- <<: *clusterDocker
environments: [cluster-docker-buildkit]
clusterDocker:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ export async function getContainerTestGarden(environmentName: string = defaultEn
}
await api.upsert({ kind: "Secret", namespace: "default", obj: authSecret, log: garden.log })
}

const credentialHelperAuth: KubernetesResource<V1Secret> = {
apiVersion: "v1",
kind: "Secret",
type: "kubernetes.io/dockerconfigjson",
metadata: {
name: "test-cred-helper-auth",
namespace: "default",
},
stringData: {
".dockerconfigjson": JSON.stringify({ credHelpers: {}, experimental: "enabled" }),
},
}
await api.upsert({ kind: "Secret", namespace: "default", obj: credentialHelperAuth, log: garden.log })
}

const provider = <KubernetesProvider>await garden.resolveProvider("local-kubernetes")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe("kubernetes container deployment handlers", () => {
})
})

it("should copy and reference imagePullSecrets", async () => {
it("should copy and reference imagePullSecrets with docker basic auth", async () => {
const service = await graph.getService("simple-service")
const secretName = "test-docker-auth"

Expand Down Expand Up @@ -148,6 +148,44 @@ describe("kubernetes container deployment handlers", () => {
expect(resource.spec.template.spec.imagePullSecrets).to.eql([{ name: secretName }])
})

it("should copy and reference imagePullSecrets with docker credential helper", async () => {
const service = await graph.getService("simple-service")
const secretName = "test-cred-helper-auth"

const authSecret: KubernetesResource<V1Secret> = {
apiVersion: "v1",
kind: "Secret",
type: "kubernetes.io/dockerconfigjson",
metadata: {
name: secretName,
namespace: "default",
},
stringData: {
".dockerconfigjson": JSON.stringify({ credHelpers: {} }),
},
}
await api.upsert({ kind: "Secret", namespace: "default", obj: authSecret, log: garden.log })

const namespace = garden.projectName
const _provider = cloneDeep(provider)
_provider.config.imagePullSecrets = [{ name: secretName, namespace: "default" }]

const resource = await createWorkloadManifest({
api,
provider: _provider,
service,
runtimeContext: emptyRuntimeContext,
namespace,
enableHotReload: false,
log: garden.log,
production: false,
})

const copiedSecret = await api.core.readNamespacedSecret(secretName, namespace)
expect(copiedSecret).to.exist
expect(resource.spec.template.spec.imagePullSecrets).to.eql([{ name: secretName }])
})

it("should correctly mount a referenced PVC module", async () => {
const service = await graph.getService("volume-reference")
const namespace = garden.projectName
Expand Down
Loading

0 comments on commit b293fe2

Please sign in to comment.