Merge pull request #2920 from somtochiama/oci-test

Add `create secret oci` command
pull/2956/head
Stefan Prodan 3 years ago committed by GitHub
commit ce2496e32c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,19 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main package main
import ( import (

@ -0,0 +1,121 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretOCICmd = &cobra.Command{
Use: "oci [name]",
Short: "Create or update a Kubernetes secret for OCI Registry authentication",
Long: `The create secret oci command generates a Kubernetes secret with `,
Example: ` # Create an OCI authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret oci podinfo-auth \
--url=ghcr.io \
--username=username \
--password=password \
--export > repo-auth.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place repo-auth.yaml
`,
RunE: createSecretOCICmdRun,
}
type secretOCIFlags struct {
url string
password string
username string
}
var secretOCIArgs = secretOCIFlags{}
func init() {
createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts")
createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username")
createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication password")
createSecretCmd.AddCommand(createSecretOCICmd)
}
func createSecretOCICmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
secretName := args[0]
if secretOCIArgs.url == "" {
return fmt.Errorf("--url is required")
}
if secretOCIArgs.username == "" {
return fmt.Errorf("--username is required")
}
if secretOCIArgs.password == "" {
return fmt.Errorf("--password is required")
}
if _, err := name.ParseReference(secretOCIArgs.url); err != nil {
return fmt.Errorf("error parsing url: '%s'", err)
}
opts := sourcesecret.Options{
Name: secretName,
Namespace: *kubeconfigArgs.Namespace,
Registry: secretOCIArgs.url,
Password: secretOCIArgs.password,
Username: secretOCIArgs.username,
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Println(secret.Content)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
}
logger.Actionf("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace)
return nil
}

@ -0,0 +1,51 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestCreateSecretOCI(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
args: "create secret oci",
assert: assertError("name is required"),
},
{
args: "create secret oci ghcr",
assert: assertError("--url is required"),
},
{
args: "create secret oci ghcr --namespace=my-namespace --url ghcr.io --username stefanprodan --password=password --export",
assert: assertGoldenFile("testdata/create_secret/oci/create-secret.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -0,0 +1,10 @@
---
apiVersion: v1
kind: Secret
metadata:
name: ghcr
namespace: my-namespace
stringData:
.dockerconfigjson: '{"auths":{"ghcr.io":{"username":"stefanprodan","password":"password","auth":"c3RlZmFucHJvZGFuOnBhc3N3b3Jk"}}}'
type: kubernetes.io/dockerconfigjson

@ -43,6 +43,7 @@ type Options struct {
Name string Name string
Namespace string Namespace string
Labels map[string]string Labels map[string]string
Registry string
SSHHostname string SSHHostname string
PrivateKeyAlgorithm PrivateKeyAlgorithm PrivateKeyAlgorithm PrivateKeyAlgorithm
RSAKeyBits int RSAKeyBits int

@ -18,6 +18,8 @@ package sourcesecret
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"net" "net"
"os" "os"
@ -36,6 +38,27 @@ import (
const defaultSSHPort = 22 const defaultSSHPort = 22
// types gotten from https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/create/create_secret_docker.go#L64-L84
// DockerConfigJSON represents a local docker auth config file
// for pulling images.
type DockerConfigJSON struct {
Auths DockerConfig `json:"auths"`
}
// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type DockerConfig map[string]DockerConfigEntry
// DockerConfigEntry holds the user information that grant the access to docker registry
type DockerConfigEntry struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
Auth string `json:"auth,omitempty"`
}
func Generate(options Options) (*manifestgen.Manifest, error) { func Generate(options Options) (*manifestgen.Manifest, error) {
var err error var err error
@ -77,7 +100,15 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
} }
} }
secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, options) var dockerCfgJson []byte
if options.Registry != "" {
dockerCfgJson, err = generateDockerConfigJson(options.Registry, options.Username, options.Password)
if err != nil {
return nil, fmt.Errorf("failed to generate json for docker config: %w", err)
}
}
secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, dockerCfgJson, options)
b, err := yaml.Marshal(secret) b, err := yaml.Marshal(secret)
if err != nil { if err != nil {
return nil, err return nil, err
@ -89,7 +120,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
}, nil }, nil
} }
func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte, options Options) (secret corev1.Secret) { func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile, dockerCfg []byte, options Options) (secret corev1.Secret) {
secret.TypeMeta = metav1.TypeMeta{ secret.TypeMeta = metav1.TypeMeta{
APIVersion: "v1", APIVersion: "v1",
Kind: "Secret", Kind: "Secret",
@ -101,6 +132,12 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte
secret.Labels = options.Labels secret.Labels = options.Labels
secret.StringData = map[string]string{} secret.StringData = map[string]string{}
if dockerCfg != nil {
secret.Type = corev1.SecretTypeDockerConfigJson
secret.StringData[corev1.DockerConfigJsonKey] = string(dockerCfg)
return
}
if options.Username != "" && options.Password != "" { if options.Username != "" && options.Password != "" {
secret.StringData[UsernameSecretKey] = options.Username secret.StringData[UsernameSecretKey] = options.Username
secret.StringData[PasswordSecretKey] = options.Password secret.StringData[PasswordSecretKey] = options.Password
@ -189,3 +226,19 @@ func resourceToString(data []byte) string {
data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1)
return string(data) return string(data)
} }
func generateDockerConfigJson(url, username, password string) ([]byte, error) {
cred := fmt.Sprintf("%s:%s", username, password)
auth := base64.StdEncoding.EncodeToString([]byte(cred))
cfg := DockerConfigJSON{
Auths: map[string]DockerConfigEntry{
url: {
Username: username,
Password: password,
Auth: auth,
},
},
}
return json.Marshal(cfg)
}

Loading…
Cancel
Save