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

Implement registries for Kubernetes backend #4092

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

meln5674
Copy link

@meln5674 meln5674 commented Sep 7, 2024

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.

* 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
@qwerty287 qwerty287 added enhancement improve existing features backend/kubernetes build_pr_images If set, the CI will build images for this PR and push to Dockerhub labels Sep 7, 2024
@6543
Copy link
Member

6543 commented Sep 7, 2024

Please also update the docs :)

Copy link
Contributor

@zc-devs zc-devs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Am I correct, that only one image pull secret is supported?
  2. 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused goos.

Copy link
Contributor

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.

Copy link
Author

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)
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Author

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": {
Copy link
Contributor

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{
Copy link
Contributor

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.

Copy link
Author

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

return nil, err
}
log.Trace().Msgf("creating secret: %s", pullSecret.Name)
_, err = engine.client.CoreV1().Secrets(engineConfig.Namespace).Create(ctx, pullSecret, meta_v1.CreateOptions{})
Copy link
Contributor

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -505,6 +514,18 @@ func startPod(ctx context.Context, engine *kube, step *types.Step, options Backe
return nil, err
}

if needsImagePullSecret(step) {
Copy link
Contributor

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.

Copy link
Author

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

@@ -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) {
Copy link
Contributor

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@zc-devs
Copy link
Contributor

zc-devs commented Sep 7, 2024

#2987

@meln5674
Copy link
Author

meln5674 commented Sep 7, 2024

Please also update the docs :)

Can do, I wasn't sure if the docs site was live at HEAD or if there was some separate release process for them.

  1. Am I correct, that only one image pull secret is supported?
  2. 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.

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
@woodpecker-bot
Copy link
Collaborator

Deploying preview to https://woodpecker-ci-woodpecker-pr-4092.surge.sh

Copy link
Contributor

@zc-devs zc-devs left a 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.

Comment on lines 92 to 115
&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,
},
Copy link
Contributor

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.

  1. For Pod there was some security/functional annotations. I don't know something like that for secrets.
  2. As name of a secret is the same as the name of Pod, we can easily find out which Pod a secret belongs to.
  3. There is no Agent-wide annotations for the volumes or the services.
  4. If there would be a real need, we can easily add it later.

Copy link
Author

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)
Copy link
Contributor

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)).

Copy link
Author

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.

Copy link
Contributor

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

Copy link
Author

@meln5674 meln5674 Sep 8, 2024

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.

Copy link
Contributor

@zc-devs zc-devs left a 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 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend/kubernetes build_pr_images If set, the CI will build images for this PR and push to Dockerhub enhancement improve existing features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants