Skip to content

Commit

Permalink
Merge pull request #49 from jetstack/registry-config-commands
Browse files Browse the repository at this point in the history
Allow generation of image pull secrets and docker config JSON from CLI
  • Loading branch information
charlieegan3 authored Nov 1, 2022
2 parents c897bbe + 5cfd22f commit d351811
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 92 deletions.
64 changes: 62 additions & 2 deletions internal/command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
"sigs.k8s.io/yaml"

"github.com/jetstack/jsctl/internal/auth"
"github.com/jetstack/jsctl/internal/client"
Expand All @@ -32,6 +34,7 @@ func registryAuth() *cobra.Command {

cmd.AddCommand(registryAuthStatus())
cmd.AddCommand(registryAuthInit())
cmd.AddCommand(registryAuthOutput())

return cmd
}
Expand All @@ -50,7 +53,7 @@ func registryAuthInit() *cobra.Command {
return fmt.Errorf("you must be logged in to run this command, run jsctl auth login")
}

fmt.Println("Checking for existing credentials in", configDir)
fmt.Fprintf(os.Stderr, "Checking for existing credentials in %s\n", configDir)

jscpClient := client.New(ctx, apiURL)

Expand Down Expand Up @@ -82,7 +85,7 @@ func registryAuthStatus() *cobra.Command {
return fmt.Errorf("no config path provided")
}

fmt.Printf("Checking for existing credentials at path: %s\n", configDir)
fmt.Fprintf(os.Stderr, "Checking for existing credentials at path: %s\n", configDir)

status, err := registry.StatusJetstackSecureEnterpriseRegistry(ctx)
if err != nil {
Expand All @@ -102,3 +105,60 @@ func registryAuthStatus() *cobra.Command {
}),
}
}

func registryAuthOutput() *cobra.Command {
var format string

cmd := &cobra.Command{
Use: "output",
Short: "output the registry credentials in various formats",
Args: cobra.ExactArgs(0),
Run: run(func(ctx context.Context, args []string) error {
var err error

_, ok := auth.TokenFromContext(ctx)
if !ok {
return fmt.Errorf("you must be logged in to run this command, run jsctl auth login")
}

jscpClient := client.New(ctx, apiURL)

keyBytes, err := registry.FetchOrLoadJetstackSecureEnterpriseRegistryCredentials(ctx, jscpClient)
if err != nil {
return err
}

if format == "json" {
fmt.Println(strings.TrimSpace(string(keyBytes)))
} else if format == "secret" {
secret, err := registry.ImagePullSecret(string(keyBytes))
if err != nil {
return fmt.Errorf("failed to create image pull secret: %s", err)
}

secretYAMLBytes, err := yaml.Marshal(secret)
if err != nil {
return fmt.Errorf("failed to marshal image pull secret: %s", err)
}

fmt.Println(strings.TrimSpace(string(secretYAMLBytes)))
} else if format == "dockerconfig" {
dockerConfigJSON, err := registry.DockerConfigJSON(string(keyBytes))
if err != nil {
return fmt.Errorf("failed to create docker config json: %s", err)
}

fmt.Println(strings.TrimSpace(string(dockerConfigJSON)))
} else {
return fmt.Errorf("unknown format: %s", format)
}

return nil
}),
}

flags := cmd.PersistentFlags()
flags.StringVar(&format, "format", "json", "Format to output the registry credentials in. Valid options are: json, secret, dockerconfig")

return cmd
}
66 changes: 5 additions & 61 deletions internal/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"bytes"
"context"
"embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -29,8 +27,8 @@ import (
"k8s.io/client-go/rest"
"sigs.k8s.io/yaml"

"github.com/jetstack/jsctl/internal/docker"
"github.com/jetstack/jsctl/internal/prompt"
"github.com/jetstack/jsctl/internal/registry"
"github.com/jetstack/jsctl/internal/venafi"
)

Expand Down Expand Up @@ -69,7 +67,7 @@ func ApplyOperatorYAML(ctx context.Context, applier Applier, options ApplyOperat
// pulled from a public registry or that the image pull secrets are already
// in place.
if options.RegistryCredentials != "" {
secret, err := ImagePullSecret(options.RegistryCredentials)
secret, err := registry.ImagePullSecret(options.RegistryCredentials)
if err != nil {
return err
}
Expand Down Expand Up @@ -187,60 +185,6 @@ func Versions() ([]string, error) {
// ErrNoKeyFile is the error given when generating an image pull secret for a key that does not exist.
var ErrNoKeyFile = errors.New("no key file")

// ImagePullSecret returns an io.Reader implementation that contains the byte representation of the Kubernetes secret
// YAML that can be used as an image pull secret for the jetstack operator. The keyData parameter should contain the JSON
// Google Service account to use in the secret.
func ImagePullSecret(keyData string) (*corev1.Secret, error) {
// When constructing a docker config for GCR, you must use the _json_key username and provide
// any valid looking email address. Methodology for building this secret was taken from the kubectl
// create secret command:
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker.go
const (
username = "_json_key"
email = "[email protected]"
)

auth := username + ":" + keyData
config := docker.ConfigJSON{
Auths: map[string]docker.ConfigEntry{
"eu.gcr.io": {
Username: username,
Password: string(keyData),
Email: email,
Auth: base64.StdEncoding.EncodeToString([]byte(auth)),
},
},
}

configJSON, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to encode docker config: %w", err)
}

const (
secretName = "jse-gcr-creds"
namespace = "jetstack-secure"
)

secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: configJSON,
},
}

return secret, nil

}

type (
// The ApplyInstallationYAMLOptions type describes additional configuration options for the operator's Installation
// custom resource.
Expand Down Expand Up @@ -321,7 +265,7 @@ func ApplyInstallationYAML(ctx context.Context, applier Applier, options ApplyIn
}

if registryCredentials != "" {
secret, err := ImagePullSecret(registryCredentials)
secret, err := registry.ImagePullSecret(registryCredentials)
if err != nil {
return fmt.Errorf("failed to parse image pull secret: %w", err)
}
Expand Down Expand Up @@ -383,11 +327,11 @@ func marshalManifests(mf *manifests) (io.Reader, error) {
// Add all Secrets to the buffer first to ensure that they get applied
// to the cluster before any Deployments that might want to use them.
for _, secret := range mf.secrets {
secretJson, err := yaml.Marshal(secret)
secretYAML, err := yaml.Marshal(secret)
if err != nil {
return nil, fmt.Errorf("failed to marshal Secret data: %w", err)
}
secretReader := bytes.NewBuffer(secretJson)
secretReader := bytes.NewBuffer(secretYAML)
if _, err = io.Copy(buf, secretReader); err != nil {
return nil, fmt.Errorf("error writing secret data to buffer: %w", err)
}
Expand Down
32 changes: 3 additions & 29 deletions internal/operator/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package operator_test
import (
"bytes"
"context"
"encoding/json"
"io"
"strings"
"testing"
Expand All @@ -15,7 +14,6 @@ import (
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"

"github.com/jetstack/jsctl/internal/docker"
"github.com/jetstack/jsctl/internal/operator"
"github.com/jetstack/jsctl/internal/venafi"
)
Expand Down Expand Up @@ -57,30 +55,6 @@ func TestVersions(t *testing.T) {
assert.NotEmpty(t, versions)
}

func TestImagePullSecret(t *testing.T) {
t.Parallel()

t.Run("It should load valid credentials and generate a secret", func(t *testing.T) {
secret, err := operator.ImagePullSecret("./testdata/key.json")
assert.NoError(t, err)

assert.EqualValues(t, "jetstack-secure", secret.Namespace)
assert.EqualValues(t, "jse-gcr-creds", secret.Name)
assert.EqualValues(t, corev1.SecretTypeDockerConfigJson, secret.Type)
assert.NotEmpty(t, secret.Data[corev1.DockerConfigJsonKey])

var actualConfig docker.ConfigJSON
assert.NoError(t, json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &actualConfig))
assert.NotEmpty(t, actualConfig.Auths)

actualGCR := actualConfig.Auths["eu.gcr.io"]
assert.NotEmpty(t, actualGCR.Email)
assert.NotEmpty(t, actualGCR.Password)
assert.NotEmpty(t, actualGCR.Auth)
assert.NotEmpty(t, actualGCR.Username)
})
}

func TestApplyInstallationYAML(t *testing.T) {
t.Parallel()
ctx := context.Background()
Expand Down Expand Up @@ -141,7 +115,7 @@ func TestApplyInstallationYAML(t *testing.T) {
applier := &TestApplier{}
options := operator.ApplyInstallationYAMLOptions{
InstallApproverPolicyEnterprise: true,
RegistryCredentialsPath: "./testdata/key.json",
RegistryCredentialsPath: "../registry/testdata/key.json",
}

err := operator.ApplyInstallationYAML(ctx, applier, options)
Expand Down Expand Up @@ -177,7 +151,7 @@ func TestApplyInstallationYAML(t *testing.T) {
applier := &TestApplier{}
options := operator.ApplyInstallationYAMLOptions{
InstallVenafiOauthHelper: true,
RegistryCredentialsPath: "./testdata/key.json",
RegistryCredentialsPath: "../registry/testdata/key.json",
}

err := operator.ApplyInstallationYAML(ctx, applier, options)
Expand Down Expand Up @@ -244,7 +218,7 @@ func TestApplyInstallationYAML(t *testing.T) {
}
options := operator.ApplyInstallationYAMLOptions{
CertDiscoveryVenafi: cdv,
RegistryCredentialsPath: "./testdata/key.json",
RegistryCredentialsPath: "../registry/testdata/key.json",
}

err := operator.ApplyInstallationYAML(ctx, applier, options)
Expand Down
74 changes: 74 additions & 0 deletions internal/registry/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package registry

import (
"encoding/base64"
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/jetstack/jsctl/internal/docker"
)

// DockerConfifJSON returns a valid docker config JSON for the given JSON Google Service Account key data
func DockerConfigJSON(keyData string) ([]byte, error) {
// When constructing a docker config for GCR, you must use the _json_key username and provide
// any valid looking email address. Methodology for building this secret was taken from the kubectl
// create secret command:
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker.go
const (
username = "_json_key"
email = "[email protected]"
)

auth := username + ":" + keyData
config := docker.ConfigJSON{
Auths: map[string]docker.ConfigEntry{
"eu.gcr.io": {
Username: username,
Password: string(keyData),
Email: email,
Auth: base64.StdEncoding.EncodeToString([]byte(auth)),
},
},
}

configJSON, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to encode docker config: %w", err)
}

return configJSON, nil
}

// ImagePullSecret returns a Kubernetes Secret resource that can be used to pull images from the Jetstack Secure
// The keyData parameter should contain the JSON Google Service account to use in the secret.
func ImagePullSecret(keyData string) (*corev1.Secret, error) {
configJSON, err := DockerConfigJSON(keyData)
if err != nil {
return nil, fmt.Errorf("failed to generate docker config: %w", err)
}

const (
secretName = "jse-gcr-creds"
namespace = "jetstack-secure"
)

secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: configJSON,
},
}

return secret, nil
}
Loading

0 comments on commit d351811

Please sign in to comment.