-
-
Notifications
You must be signed in to change notification settings - Fork 354
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
Implement registries for Kubernetes backend #4092
base: main
Are you sure you want to change the base?
Implement registries for Kubernetes backend #4092
Conversation
* For each step that matches an registry, create a secret with the same name as its pod * Delete this same secret when the pod is deleted
Please also update the docs :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Am I correct, that only one image pull secret is supported?
- There was
registry
field in UI, as I remember. You don't use it, right?
https://woodpecker-ci.org/docs/usage/registries#images-from-private-registries
Woodpecker matches the registry hostname to each image in your YAML. If the hostnames match, the registry credentials are used to authenticate to your registry and pull the image.
Seems, registries are checked on the Server. As you create/delete secret each time, it should work as intended and comments above are invalid.
However, I still believe, that creating one secret with all available registries/auths and then "attach" it (via imagePullSecret
) to needed steps/pods is a better approach.
return step.AuthConfig.Username != "" && step.AuthConfig.Password != "" | ||
} | ||
|
||
func mkImagePullSecret(step *types.Step, config *config, podName, goos string, options BackendOptions) (*v1.Secret, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused goos
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is secrets.go
. Secrets stuff should be there, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed goos, moved all secrets-related func's to secrets.go
if err != nil { | ||
return nil, err | ||
} | ||
annotations := podAnnotations(config, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use Pod annotations on secret?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, applied to labels also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed the user would want to label and annotate the secrets as well, but if you prefer a second pair of env vars for the secrets, I can do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 4 new env's for labels, annotations, and allow for both for registry secrets. Added separate func's to return labels and annotations specifically for registry secrets.
"part-of": "woodpecker-ci", | ||
"step": "go-test" | ||
}, | ||
"annotations": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I'm talking about. They are Pod specific.
} | ||
}` | ||
|
||
pullSecret, err := mkImagePullSecret(&types.Step{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need a half of options to test secrets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed all non-relevant fields
pipeline/backend/kubernetes/pod.go
Outdated
return nil, err | ||
} | ||
log.Trace().Msgf("creating secret: %s", pullSecret.Name) | ||
_, err = engine.client.CoreV1().Secrets(engineConfig.Namespace).Create(ctx, pullSecret, meta_v1.CreateOptions{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make a function in image_pull_secrets
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
pipeline/backend/kubernetes/pod.go
Outdated
@@ -505,6 +514,18 @@ func startPod(ctx context.Context, engine *kube, step *types.Step, options Backe | |||
return nil, err | |||
} | |||
|
|||
if needsImagePullSecret(step) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And probably call it in kubernetes.go
. See how volumes work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created startRegistrySecret and stopRegistrySecret to mirror startPod/Volume and stopPod/Volume
pipeline/backend/kubernetes/pod.go
Outdated
@@ -514,12 +535,23 @@ func stopPod(ctx context.Context, engine *kube, step *types.Step, deleteOpts met | |||
if err != nil { | |||
return err | |||
} | |||
errs := make([]error, 0, 2) | |||
if needsImagePullSecret(step) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same as two above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Can do, I wasn't sure if the docs site was live at HEAD or if there was some separate release process for them.
In Kubernetes? No, any number of secrets is supported, with any number of auths in each one . In woodpecker, I saw that the AuthConfig for a step is a single user:pass pair, which is matched against the list of repo, org, and global registries, so in that sense, a single extra secret is supported. That said, in the future, if sidecars are also supported, it may make sense for this one secret to contain multiple auths. Because you can have as many imagePullSecrets as you want in a pod, it makes much more sense to list the user-managed ones and then the agent-managed ones, instead of reading them from the API server and writing them back out, as the kubelet will do that for you, and retry against each one in sequence in the event of authentication errors. |
* Updated docs to remove unsupported functionality note and fix grammar * Use term "registry secret" in source to refer to temporary secrets created to contain registry authentication * Added separate env's to add extra labels and annotations to registry secrets * Consolidated registry secret functions into secrets.go * Moved secret creation/cleanup logic to separate functions in secret.go, called in kubernetes.go * Fixed whitespace errors and unused fields in test code
Deploying preview to https://woodpecker-ci-woodpecker-pr-4092.surge.sh |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, looks very well to me.
Am I correct, that only one image pull secret is supported?
There was registry field in UI, as I remember. You don't use it, right?
I was confused by flashbacks from #3122. I thought, if registry isn't passed within AuthConfig, then how Kubernetes would choose right secret? Or the only one secret/registry would be supported.
Then I reread through my PR
Made the secrets pipeline-wide instead of step-wide as in Docker backend
and understood everything :) ⬇️
registries are checked on the Server. As you create/delete secret each time, it should work as intended
--
if sidecars are also supported, it may make sense for this one secret to contain multiple auths
Good catch.
instead of reading them from the API server and writing them back out
Don't get this. Who reads regcreds from API? Kubernetes API? What is the "back"?
kubelet will do that for you, and retry against each one
That is exactly, what I had in mind, implementing #3122. ⬇️
However, I still believe, that creating one secret with all available registries/auths and then "attach" it (via imagePullSecret) to needed steps/pods is a better approach.
Yeah, maybe even not to "needed" pods, but all of them. And let Kubernetes cope with that.
pipeline/backend/kubernetes/flags.go
Outdated
&cli.StringFlag{ | ||
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_REGISTRY_SECRET_LABELS"), | ||
Name: "backend-k8s-registry-secret-labels", | ||
Usage: "backend k8s additional Agent-wide registry image pull secret labels", | ||
Value: "", | ||
}, | ||
&cli.BoolFlag{ | ||
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_REGISTRY_SECRET_LABELS_ALLOW_FROM_STEP"), | ||
Name: "backend-k8s-registry-secret-labels-allow-from-step", | ||
Usage: "whether to allow using labels from step's backend options for registry image pull secrets", | ||
Value: false, | ||
}, | ||
&cli.StringFlag{ | ||
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_REGISTRY_SECRET_ANNOTATIONS"), | ||
Name: "backend-k8s-registry-secret-annotations", | ||
Usage: "backend k8s additional Agent-wide registry image pull secret annotations", | ||
Value: "", | ||
}, | ||
&cli.BoolFlag{ | ||
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_REGISTRY_SECRET_ANNOTATIONS_ALLOW_FROM_STEP"), | ||
Name: "backend-k8s-registry-secret-annotations-allow-from-step", | ||
Usage: "whether to allow using annotations from step's backend options for registry image pull secret", | ||
Value: false, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed the user would want to label and annotate the secrets as well
Added 4 new env's for labels, annotations, and allow for both for registry secrets
I would remove it, don't see a reason to keep it.
- For Pod there was some security/functional annotations. I don't know something like that for secrets.
- As name of a secret is the same as the name of Pod, we can easily find out which Pod a secret belongs to.
- There is no Agent-wide annotations for the volumes or the services.
- If there would be a real need, we can easily add it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. I tend to err on the side of flexibility with these sorts of things, but I guess it is in the commit history if we want it back.
Done.
}, | ||
} | ||
|
||
configFileJSON, err := json.Marshal(authConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it work without encoding to Base64?
If works, then OK. Otherwise, maybe we can generalize code with Docker (#3122, func (a Auth) EncodeToBase64() (string, error)
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. If you're thinking of needing to use base64 in secret YAML files, that is accomplished by the encoding/json package "Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string". The serialized JSON form contains a base64 string, but the k8s data structure contains raw bytes.
I've tested prior to each push with a full install into a k8s cluster, both testing that a private image fails without the registry, and succeeds with it, so if it didn't work without base64, that would have caught it. I'm happy to share or commit this test setup script if you'd like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant more auth
field in the auths
, than entire .dockerconfigjson
in the secret.
https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
{"auths":{"your.private.registry.example.com":{"username":"janedoe","password":"xxxxxxxxxxx","email":"[email protected]","auth":"c3R...zE2"}}}
To understand what is in the auth field, convert the base64-encoded data to a readable format:
$ echo "c3R...zE2" | base64 --decode
janedoe:xxxxxxxxxxx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. In k8s, as far as I know, that field isn't actually required, and empirically, it works without it.
But don't take my word for it, here's the tests from kubernetes itself where they work with just username and password.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it is tested and works (#4092 (comment)), no more comments from me.
Thank you for the contribution 🎉
According to the documentation, per-organization and per-pipeline registries are currently unsupported for the Kubernetes backend.
This patch implements this missing functionality by creating and deleting a matching secret for each pod with a matched registry, using the same name, labels, and annotations as the pod, and appending it to its
imagePullSecrets
list.This patch adds tests for the new functionality, and has been manually end-to-end-tested in KinD by using a private image hosted in the matching gitea instance.
This will require updating the matching helm charts to add the create/delete permissions to the agent role, which I am happy to do as well.