diff --git a/cmd/kops/create_secret.go b/cmd/kops/create_secret.go index fe416514d0b66..9b1eb7b7e1cfc 100644 --- a/cmd/kops/create_secret.go +++ b/cmd/kops/create_secret.go @@ -33,6 +33,9 @@ var ( # Create an new ssh public key called admin. kops create secret sshpublickey admin -i ~/.ssh/id_rsa.pub \ --name k8s-cluster.example.com --state s3://example.com + + kops create secret dockerconfig -f ~/.docker/config.json \ + --name k8s-cluster.example.com --state s3://example.com `)) create_secret_short = i18n.T(`Create a secret.`) @@ -48,6 +51,7 @@ func NewCmdCreateSecret(f *util.Factory, out io.Writer) *cobra.Command { // create subcommands cmd.AddCommand(NewCmdCreateSecretPublicKey(f, out)) + cmd.AddCommand(NewCmdCreateSecretDockerConfig(f, out)) return cmd } diff --git a/cmd/kops/create_secret_dockerconfig.go b/cmd/kops/create_secret_dockerconfig.go new file mode 100644 index 0000000000000..c300c3738ae27 --- /dev/null +++ b/cmd/kops/create_secret_dockerconfig.go @@ -0,0 +1,124 @@ +/* +Copyright 2016 The Kubernetes 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 ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + "k8s.io/kops/cmd/kops/util" + "k8s.io/kops/pkg/apis/kops/registry" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + "k8s.io/kubernetes/pkg/util/i18n" +) + +var ( + create_secret_dockerconfig_long = templates.LongDesc(i18n.T(` + Create a new docker config, and store it in the state store. + Used to configure docker on each master or node (ie. for auth) + Use update to modify it, this command will only create a new entry.`)) + + create_secret_dockerconfig_example = templates.Examples(i18n.T(` + # Create an new docker config. + kops create secret dockerconfig -f /path/to/docker/config.json \ + --name k8s-cluster.example.com --state s3://example.com + `)) + + create_secret_dockerconfig_short = i18n.T(`Create a docker config.`) +) + +type CreateSecretDockerConfigOptions struct { + ClusterName string + DockerConfigPath string +} + +func NewCmdCreateSecretDockerConfig(f *util.Factory, out io.Writer) *cobra.Command { + options := &CreateSecretDockerConfigOptions{} + + cmd := &cobra.Command{ + Use: "dockerconfig", + Short: create_secret_dockerconfig_short, + Long: create_secret_dockerconfig_long, + Example: create_secret_dockerconfig_example, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + exitWithError(fmt.Errorf("syntax: -f ")) + } + + err := rootCommand.ProcessArgs(args[0:]) + if err != nil { + exitWithError(err) + } + + options.ClusterName = rootCommand.ClusterName() + + err = RunCreateSecretDockerConfig(f, os.Stdout, options) + if err != nil { + exitWithError(err) + } + }, + } + + cmd.Flags().StringVarP(&options.DockerConfigPath, "", "f", "", "Path to docker config JSON file") + + return cmd +} + +func RunCreateSecretDockerConfig(f *util.Factory, out io.Writer, options *CreateSecretDockerConfigOptions) error { + if options.DockerConfigPath == "" { + return fmt.Errorf("docker config path is required (use -f)") + } + secret, err := fi.CreateSecret() + if err != nil { + return fmt.Errorf("error creating docker config secret: %v", err) + } + + cluster, err := GetCluster(f, options.ClusterName) + if err != nil { + return err + } + + secretStore, err := registry.SecretStore(cluster) + if err != nil { + return err + } + + data, err := ioutil.ReadFile(options.DockerConfigPath) + if err != nil { + return fmt.Errorf("error reading docker config %v: %v", options.DockerConfigPath, err) + } + + var parsedData map[string]interface{} + err = json.Unmarshal(data, &parsedData) + if err != nil { + return fmt.Errorf("Unable to parse JSON %v: %v", options.DockerConfigPath, err) + } + + secret.Data = data + + _, _, err = secretStore.GetOrCreateSecret("dockerconfig", secret) + if err != nil { + return fmt.Errorf("error adding docker config secret: %v", err) + } + + return nil +} diff --git a/docs/cli/kops_create_secret.md b/docs/cli/kops_create_secret.md index 27e7b55d57e19..2421473cf1069 100644 --- a/docs/cli/kops_create_secret.md +++ b/docs/cli/kops_create_secret.md @@ -16,6 +16,9 @@ Create a secret # Create an new ssh public key called admin. kops create secret sshpublickey admin -i ~/.ssh/id_rsa.pub \ --name k8s-cluster.example.com --state s3://example.com + + kops create secret dockerconfig -f ~/.docker/config.json \ + --name k8s-cluster.example.com --state s3://example.com ``` ### Options inherited from parent commands @@ -35,5 +38,6 @@ Create a secret ### SEE ALSO * [kops create](kops_create.md) - Create a resource by command line, filename or stdin. +* [kops create secret dockerconfig](kops_create_secret_dockerconfig.md) - Create a docker config. * [kops create secret sshpublickey](kops_create_secret_sshpublickey.md) - Create a ssh public key. diff --git a/docs/cli/kops_create_secret_dockerconfig.md b/docs/cli/kops_create_secret_dockerconfig.md new file mode 100644 index 0000000000000..1db22b2ac4e35 --- /dev/null +++ b/docs/cli/kops_create_secret_dockerconfig.md @@ -0,0 +1,48 @@ + + + +## kops create secret dockerconfig + +Create a docker config. + +### Synopsis + + +Create a new docker config, and store it in the state store. Used to configure docker on each master or node (ie. for auth) Use update to modify it, this command will only create a new entry. + +``` +kops create secret dockerconfig +``` + +### Examples + +``` + # Create an new docker config. + kops create secret dockerconfig -f /path/to/docker/config.json \ + --name k8s-cluster.example.com --state s3://example.com +``` + +### Options + +``` + -f, -- string Path to docker config JSON file +``` + +### Options inherited from parent commands + +``` + --alsologtostderr log to standard error as well as files + --config string config file (default is $HOME/.kops.yaml) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --logtostderr log to standard error instead of files (default false) + --name string Name of cluster + --state string Location of state storage + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level log level for V logs + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO +* [kops create secret](kops_create_secret.md) - Create a secret. + diff --git a/docs/security.md b/docs/security.md index b3637fe310d98..abba7215766fd 100644 --- a/docs/security.md +++ b/docs/security.md @@ -20,6 +20,15 @@ To change the SSH public key on an existing cluster: * `kops update cluster --yes` to reconfigure the auto-scaling groups * `kops rolling-update cluster --name --yes` to immediately roll all the machines so they have the new key (optional) +## Docker Configuration + +If you are using a private registry such as quay.io, you may be familiar with the inconvenience of managing the `imagePullSecrets` for each namespace. It can also be a pain to use [Kops Hooks](cluster_spec.md#hooks) with private images. To configure docker on all nodes with access to one or more private registries: + +* `kops create secret --name dockerconfig -f ~/.docker/config.json` +* `kops rolling-update cluster --name --yes` to immediately roll all the machines so they have the new key (optional) + +This stores the [config.json](https://docs.docker.com/engine/reference/commandline/login/) in `/root/.docker/config.json` on all nodes (include masters) so that both Kubernetes and system containers may use registries defined in it. + ## IAM roles All Pods running on your cluster have access to underlying instance IAM role. diff --git a/nodeup/pkg/model/kubelet.go b/nodeup/pkg/model/kubelet.go index 1ecc3f35e7f94..13aa362c8726c 100644 --- a/nodeup/pkg/model/kubelet.go +++ b/nodeup/pkg/model/kubelet.go @@ -146,6 +146,8 @@ func (b *KubeletBuilder) buildSystemdEnvironmentFile(kubeletConfig *kops.Kubelet } sysconfig := "DAEMON_ARGS=\"" + flags + "\"\n" + // Makes kubelet read /root/.docker/config.json properly + sysconfig = sysconfig + "HOME=\"/root" + "\"\n" t := &nodetasks.File{ Path: "/etc/sysconfig/kubelet", diff --git a/nodeup/pkg/model/secrets.go b/nodeup/pkg/model/secrets.go index 3ad84f42a8aa5..24aa96b4b87fb 100644 --- a/nodeup/pkg/model/secrets.go +++ b/nodeup/pkg/model/secrets.go @@ -58,6 +58,21 @@ func (b *SecretBuilder) Build(c *fi.ModelBuilderContext) error { c.AddTask(t) } + if b.SecretStore != nil { + key := "dockerconfig" + dockercfg, _ := b.SecretStore.Secret(key) + if dockercfg != nil { + contents := string(dockercfg.Data) + t := &nodetasks.File{ + Path: filepath.Join("root", ".docker", "config.json"), + Contents: fi.NewStringResource(contents), + Type: nodetasks.FileType_File, + Mode: s("0600"), + } + c.AddTask(t) + } + } + // if we are not a master we can stop here if !b.IsMaster { return nil @@ -129,6 +144,9 @@ func (b *SecretBuilder) Build(c *fi.ModelBuilderContext) error { var lines []string for id, token := range allTokens { + if id == "dockerconfig" { + continue + } lines = append(lines, token+","+id+","+id) } csv := strings.Join(lines, "\n") diff --git a/nodeup/pkg/model/tests/kubelet/featuregates/tasks.yaml b/nodeup/pkg/model/tests/kubelet/featuregates/tasks.yaml index 585a3aae16124..a0d60b74f39a2 100644 --- a/nodeup/pkg/model/tests/kubelet/featuregates/tasks.yaml +++ b/nodeup/pkg/model/tests/kubelet/featuregates/tasks.yaml @@ -1,4 +1,5 @@ contents: | DAEMON_ARGS="--feature-gates=AllowExtTrafficLocalEndpoints=false,ExperimentalCriticalPodAnnotation=true --node-labels=kubernetes.io/role=node,node-role.kubernetes.io/node= --cni-bin-dir=/opt/cni/bin/ --cni-conf-dir=/etc/cni/net.d/ --network-plugin-dir=/opt/cni/bin/" + HOME="/root" path: /etc/sysconfig/kubelet type: file