mirror of https://github.com/kubernetes/kops.git
Merge pull request #12055 from johngmyers/complete-secret
Clean up "create secret" subcommands
This commit is contained in:
commit
85c2513606
|
|
@ -8,10 +8,10 @@ go_library(
|
|||
"create_instancegroup.go",
|
||||
"create_keypair.go",
|
||||
"create_secret.go",
|
||||
"create_secret_cilium_encryptionconfig.go",
|
||||
"create_secret_ciliumpassword.go",
|
||||
"create_secret_dockerconfig.go",
|
||||
"create_secret_encryptionconfig.go",
|
||||
"create_secret_weave_encryptionconfig.go",
|
||||
"create_secret_weavepassword.go",
|
||||
"create_sshpublickey.go",
|
||||
"delete.go",
|
||||
"delete_cluster.go",
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ func NewCmdCreateSecret(f *util.Factory, out io.Writer) *cobra.Command {
|
|||
}
|
||||
|
||||
// create subcommands
|
||||
cmd.AddCommand(NewCmdCreateSecretCiliumPassword(f, out))
|
||||
cmd.AddCommand(NewCmdCreateSecretDockerConfig(f, out))
|
||||
cmd.AddCommand(NewCmdCreateSecretEncryptionConfig(f, out))
|
||||
cmd.AddCommand(NewCmdCreateSecretWeaveEncryptionConfig(f, out))
|
||||
cmd.AddCommand(NewCmdCreateSecretCiliumEncryptionConfig(f, out))
|
||||
cmd.AddCommand(NewCmdCreateSecretWeavePassword(f, out))
|
||||
|
||||
sshPublicKey := NewCmdCreateSSHPublicKey(f, out)
|
||||
sshPublicKey.Hidden = true
|
||||
|
|
|
|||
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kops/cmd/kops/util"
|
||||
"k8s.io/kops/pkg/apis/kops"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
createSecretCiliumEncryptionconfigLong = templates.LongDesc(i18n.T(`
|
||||
Create a new cilium encryption secret, and store it in the state store.
|
||||
Used by Cilium to generate encrypted communication between pods/nodes.`))
|
||||
|
||||
createSecretCiliumEncryptionconfigExample = templates.Examples(i18n.T(`
|
||||
# Create a new cilium encryption key.
|
||||
kops create secret ciliumpassword -f /path/to/ciliumpassword \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
# Create a new cilium encryption key via stdin.
|
||||
cat <<EOF | kops create secret ciliumpassword --name k8s-cluster.example.com --state s3://my-state-store -f -
|
||||
keys: $(echo "3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2> /dev/null| xxd -p -c 64)) 128")
|
||||
EOF
|
||||
# Replace an existing ciliumpassword secret
|
||||
kops create secret ciliumpassword -f /path/to/ciliumpassword --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
`))
|
||||
|
||||
createSecretCiliumEncryptionconfigShort = i18n.T(`Create a cilium encryption key.`)
|
||||
)
|
||||
|
||||
type CreateSecretCiliumEncryptionConfigOptions struct {
|
||||
ClusterName string
|
||||
CiliumPasswordFilePath string
|
||||
Force bool
|
||||
}
|
||||
|
||||
func NewCmdCreateSecretCiliumEncryptionConfig(f *util.Factory, out io.Writer) *cobra.Command {
|
||||
options := &CreateSecretCiliumEncryptionConfigOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ciliumpassword",
|
||||
Short: createSecretCiliumEncryptionconfigShort,
|
||||
Long: createSecretCiliumEncryptionconfigLong,
|
||||
Example: createSecretCiliumEncryptionconfigExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.TODO()
|
||||
|
||||
if len(args) != 0 {
|
||||
exitWithError(fmt.Errorf("syntax: -f <CiliumPasswordFilePath>"))
|
||||
}
|
||||
|
||||
err := rootCommand.ProcessArgs(args[0:])
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
|
||||
options.ClusterName = rootCommand.ClusterName(true)
|
||||
|
||||
err = RunCreateSecretCiliumEncryptionConfig(ctx, f, os.Stdout, options)
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.CiliumPasswordFilePath, "", "f", "", "Path to the cilium encryption config file")
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the kOps secret if it already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RunCreateSecretCiliumEncryptionConfig(ctx context.Context, f *util.Factory, out io.Writer, options *CreateSecretCiliumEncryptionConfigOptions) error {
|
||||
if options.CiliumPasswordFilePath == "" {
|
||||
return fmt.Errorf("cilium encryption config path is required (use -f)")
|
||||
}
|
||||
|
||||
secret, err := fi.CreateSecret()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating encryption secret: %v", err)
|
||||
}
|
||||
|
||||
cluster, err := GetCluster(ctx, f, options.ClusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientset, err := f.Clientset()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretStore, err := clientset.SecretStore(cluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data []byte
|
||||
if options.CiliumPasswordFilePath == "-" {
|
||||
data, err = ConsumeStdin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading cilium encryption config from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(options.CiliumPasswordFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading encryption config %v: %v", options.CiliumPasswordFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
var parsedData map[string]interface{}
|
||||
err = kops.ParseRawYaml(data, &parsedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse yaml %v: %v", options.CiliumPasswordFilePath, err)
|
||||
}
|
||||
|
||||
secret.Data = data
|
||||
|
||||
if !options.Force {
|
||||
_, created, err := secretStore.GetOrCreateSecret("ciliumpassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding ciliumpassword secret: %v", err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("failed to create the encryptionconfig secret as it already exists. The `--force` flag can be passed to replace an existing secret")
|
||||
}
|
||||
} else {
|
||||
_, err := secretStore.ReplaceSecret("ciliumpassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating ciliumpassword secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
Copyright 2019 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kops/cmd/kops/util"
|
||||
"k8s.io/kops/pkg/apis/kops"
|
||||
"k8s.io/kops/pkg/commands/commandutils"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
createSecretCiliumPasswordLong = templates.LongDesc(i18n.T(`
|
||||
Create a new Cilium IPsec configuration and store it in the state store.
|
||||
This is used by Cilium to encrypt communication between pods/nodes.`))
|
||||
|
||||
createSecretCiliumPasswordExample = templates.Examples(i18n.T(`
|
||||
# Create a new Cilium IPsec configuration.
|
||||
kops create secret ciliumpassword -f /path/to/configuration.yaml \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Create a new Cilium IPsec key via stdin.
|
||||
cat <<EOF | kops create secret ciliumpassword --name k8s-cluster.example.com --state s3://my-state-store -f -
|
||||
keys: $(echo "3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2> /dev/null| xxd -p -c 64)) 128")
|
||||
EOF
|
||||
|
||||
# Replace an existing Cilium IPsec configuration secret
|
||||
kops create secret ciliumpassword -f /path/to/configuration.yaml --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
`))
|
||||
|
||||
createSecretCiliumPasswordShort = i18n.T(`Create a Cilium IPsec configuration.`)
|
||||
)
|
||||
|
||||
type CreateSecretCiliumPasswordOptions struct {
|
||||
ClusterName string
|
||||
CiliumPasswordFilePath string
|
||||
Force bool
|
||||
}
|
||||
|
||||
func NewCmdCreateSecretCiliumPassword(f *util.Factory, out io.Writer) *cobra.Command {
|
||||
options := &CreateSecretCiliumPasswordOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ciliumpassword [CLUSTER] -f FILENAME",
|
||||
Short: createSecretCiliumPasswordShort,
|
||||
Long: createSecretCiliumPasswordLong,
|
||||
Example: createSecretCiliumPasswordExample,
|
||||
Args: rootCommand.clusterNameArgs(&options.ClusterName),
|
||||
ValidArgsFunction: commandutils.CompleteClusterName(&rootCommand, true, false),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunCreateSecretCiliumEncryptionConfig(context.TODO(), f, out, options)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.CiliumPasswordFilePath, "filename", "f", "", "Path to the Cilium IPsec configuration file")
|
||||
cmd.MarkFlagRequired("filename")
|
||||
cmd.RegisterFlagCompletionFunc("filename", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
|
||||
})
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the secret if it already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RunCreateSecretCiliumEncryptionConfig(ctx context.Context, f *util.Factory, out io.Writer, options *CreateSecretCiliumPasswordOptions) error {
|
||||
cluster, err := GetCluster(ctx, f, options.ClusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientset, err := f.Clientset()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretStore, err := clientset.SecretStore(cluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if options.CiliumPasswordFilePath == "-" {
|
||||
data, err = ConsumeStdin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading Cilium IPSec config from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(options.CiliumPasswordFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading Cilium IPSec config %v: %v", options.CiliumPasswordFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
var parsedData map[string]interface{}
|
||||
err = kops.ParseRawYaml(data, &parsedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML %v: %v", options.CiliumPasswordFilePath, err)
|
||||
}
|
||||
|
||||
secret := &fi.Secret{
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if !options.Force {
|
||||
_, created, err := secretStore.GetOrCreateSecret("ciliumpassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding Cilium IPSec secret: %v", err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("failed to create the Cilium IPSec secret as it already exists. Pass the `--force` flag to replace an existing secret")
|
||||
}
|
||||
} else {
|
||||
_, err := secretStore.ReplaceSecret("ciliumpassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating Cilium IPSec secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -22,39 +22,41 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kops/cmd/kops/util"
|
||||
"k8s.io/kops/pkg/commands/commandutils"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
createSecretDockerconfigLong = 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 (i.e. for auth)
|
||||
Use update to modify it, this command will only create a new entry.
|
||||
createSecretDockerConfigLong = templates.LongDesc(i18n.T(`
|
||||
Create a new Docker config and store it in the state store.
|
||||
Used to configure Docker authentication on each node.
|
||||
|
||||
After creating a dockerconfig secret, a /root/.docker/config.json file
|
||||
After creating a dockerconfig secret a /root/.docker/config.json file
|
||||
will be added to newly created nodes. This file will be used by Kubernetes
|
||||
to authenticate to container registries and will also work when using
|
||||
containerd as container runtime.`))
|
||||
to authenticate to container registries.
|
||||
|
||||
createSecretDockerconfigExample = templates.Examples(i18n.T(`
|
||||
# Create a new docker config.
|
||||
This will also work when using containerd as the container runtime.`))
|
||||
|
||||
createSecretDockerConfigExample = templates.Examples(i18n.T(`
|
||||
# Create a new Docker config.
|
||||
kops create secret dockerconfig -f /path/to/docker/config.json \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Create a docker config via stdin.
|
||||
generate-docker-config.sh | kops create secret dockerconfig -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Replace an existing docker config secret.
|
||||
kops create secret dockerconfig -f /path/to/docker/config.json --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
`))
|
||||
|
||||
createSecretDockerconfigShort = i18n.T(`Create a docker config.`)
|
||||
createSecretDockerConfigShort = i18n.T(`Create a Docker config.`)
|
||||
)
|
||||
|
||||
type CreateSecretDockerConfigOptions struct {
|
||||
|
|
@ -67,46 +69,28 @@ func NewCmdCreateSecretDockerConfig(f *util.Factory, out io.Writer) *cobra.Comma
|
|||
options := &CreateSecretDockerConfigOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dockerconfig",
|
||||
Short: createSecretDockerconfigShort,
|
||||
Long: createSecretDockerconfigLong,
|
||||
Example: createSecretDockerconfigExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.TODO()
|
||||
|
||||
if len(args) != 0 {
|
||||
exitWithError(fmt.Errorf("syntax: -f <DockerConfigPath>"))
|
||||
}
|
||||
|
||||
err := rootCommand.ProcessArgs(args[0:])
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
|
||||
options.ClusterName = rootCommand.ClusterName(true)
|
||||
|
||||
err = RunCreateSecretDockerConfig(ctx, f, os.Stdout, options)
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
Use: "dockerconfig [CLUSTER] -f FILENAME",
|
||||
Short: createSecretDockerConfigShort,
|
||||
Long: createSecretDockerConfigLong,
|
||||
Example: createSecretDockerConfigExample,
|
||||
Args: rootCommand.clusterNameArgs(&options.ClusterName),
|
||||
ValidArgsFunction: commandutils.CompleteClusterName(&rootCommand, true, false),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunCreateSecretDockerConfig(context.TODO(), f, out, options)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.DockerConfigPath, "", "f", "", "Path to docker config JSON file")
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the kOps secret if it already exists")
|
||||
cmd.Flags().StringVarP(&options.DockerConfigPath, "filename", "f", "", "Path to Docker config JSON file")
|
||||
cmd.MarkFlagRequired("filename")
|
||||
cmd.RegisterFlagCompletionFunc("filename", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"json"}, cobra.ShellCompDirectiveFilterFileExt
|
||||
})
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the secret if it already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RunCreateSecretDockerConfig(ctx context.Context, 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(ctx, f, options.ClusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -121,39 +105,42 @@ func RunCreateSecretDockerConfig(ctx context.Context, f *util.Factory, out io.Wr
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if options.DockerConfigPath == "-" {
|
||||
data, err = ConsumeStdin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading docker config from stdin: %v", err)
|
||||
return fmt.Errorf("reading Docker config from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(options.DockerConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading docker config %v: %v", options.DockerConfigPath, err)
|
||||
return fmt.Errorf("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)
|
||||
return fmt.Errorf("unable to parse JSON %v: %v", options.DockerConfigPath, err)
|
||||
}
|
||||
|
||||
secret.Data = data
|
||||
secret := &fi.Secret{
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if !options.Force {
|
||||
_, created, err := secretStore.GetOrCreateSecret("dockerconfig", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding dockerconfig secret: %v", err)
|
||||
return fmt.Errorf("adding dockerconfig secret: %v", err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("failed to create the dockerconfig secret as it already exists. The `--force` flag can be passed to replace an existing secret.")
|
||||
return fmt.Errorf("failed to create the dockerconfig secret as it already exists. Pass the `--force` flag to replace an existing secret")
|
||||
}
|
||||
} else {
|
||||
_, err := secretStore.ReplaceSecret("dockerconfig", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating dockerconfig secret: %v", err)
|
||||
return fmt.Errorf("updating dockerconfig secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,35 +21,36 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kops/cmd/kops/util"
|
||||
"k8s.io/kops/pkg/apis/kops"
|
||||
"k8s.io/kops/pkg/commands/commandutils"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
createSecretEncryptionconfigLong = templates.LongDesc(i18n.T(`
|
||||
Create a new encryption config, and store it in the state store.
|
||||
Used to configure encryption-at-rest by the kube-apiserver process
|
||||
on each of the master nodes. The config is not updated by this command.`))
|
||||
createSecretEncryptionConfigLong = templates.LongDesc(i18n.T(`
|
||||
Create a new encryption config and store it in the state store.
|
||||
Used to configure encryption-at-rest by the kube-apiserver process.`))
|
||||
|
||||
createSecretEncryptionconfigExample = templates.Examples(i18n.T(`
|
||||
createSecretEncryptionConfigExample = templates.Examples(i18n.T(`
|
||||
# Create a new encryption config.
|
||||
kops create secret encryptionconfig -f config.yaml \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Create a new encryption config via stdin.
|
||||
generate-encryption-config.sh | kops create secret encryptionconfig -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Replace an existing encryption config secret.
|
||||
kops create secret encryptionconfig -f config.yaml --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
`))
|
||||
|
||||
createSecretEncryptionconfigShort = i18n.T(`Create an encryption config.`)
|
||||
createSecretEncryptionConfigShort = i18n.T(`Create an encryption config.`)
|
||||
)
|
||||
|
||||
type CreateSecretEncryptionConfigOptions struct {
|
||||
|
|
@ -62,47 +63,28 @@ func NewCmdCreateSecretEncryptionConfig(f *util.Factory, out io.Writer) *cobra.C
|
|||
options := &CreateSecretEncryptionConfigOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "encryptionconfig",
|
||||
Short: createSecretEncryptionconfigShort,
|
||||
Long: createSecretEncryptionconfigLong,
|
||||
Example: createSecretEncryptionconfigExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.TODO()
|
||||
|
||||
if len(args) != 0 {
|
||||
exitWithError(fmt.Errorf("syntax: -f <EncryptionConfigPath>"))
|
||||
}
|
||||
|
||||
err := rootCommand.ProcessArgs(args[0:])
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
|
||||
options.ClusterName = rootCommand.ClusterName(true)
|
||||
|
||||
err = RunCreateSecretEncryptionConfig(ctx, f, os.Stdout, options)
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
Use: "encryptionconfig [CLUSTER] -f FILENAME",
|
||||
Short: createSecretEncryptionConfigShort,
|
||||
Long: createSecretEncryptionConfigLong,
|
||||
Example: createSecretEncryptionConfigExample,
|
||||
Args: rootCommand.clusterNameArgs(&options.ClusterName),
|
||||
ValidArgsFunction: commandutils.CompleteClusterName(&rootCommand, true, false),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunCreateSecretEncryptionConfig(context.TODO(), f, out, options)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.EncryptionConfigPath, "", "f", "", "Path to encryption config yaml file")
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the kOps secret if it already exists")
|
||||
cmd.Flags().StringVarP(&options.EncryptionConfigPath, "filename", "f", "", "Path to encryption config YAML file")
|
||||
cmd.MarkFlagRequired("filename")
|
||||
cmd.RegisterFlagCompletionFunc("filename", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
|
||||
})
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the secret if it already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RunCreateSecretEncryptionConfig(ctx context.Context, f *util.Factory, out io.Writer, options *CreateSecretEncryptionConfigOptions) error {
|
||||
if options.EncryptionConfigPath == "" {
|
||||
return fmt.Errorf("encryption config path is required (use -f)")
|
||||
}
|
||||
|
||||
secret, err := fi.CreateSecret()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating encryption config secret: %v", err)
|
||||
}
|
||||
|
||||
cluster, err := GetCluster(ctx, f, options.ClusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -121,35 +103,37 @@ func RunCreateSecretEncryptionConfig(ctx context.Context, f *util.Factory, out i
|
|||
if options.EncryptionConfigPath == "-" {
|
||||
data, err = ConsumeStdin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading encryption config from stdin: %v", err)
|
||||
return fmt.Errorf("reading encryption config from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(options.EncryptionConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading encryption config %v: %v", options.EncryptionConfigPath, err)
|
||||
return fmt.Errorf("reading encryption config %v: %v", options.EncryptionConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
var parsedData map[string]interface{}
|
||||
err = kops.ParseRawYaml(data, &parsedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse yaml %v: %v", options.EncryptionConfigPath, err)
|
||||
return fmt.Errorf("unable to parse YAML %v: %v", options.EncryptionConfigPath, err)
|
||||
}
|
||||
|
||||
secret.Data = data
|
||||
secret := &fi.Secret{
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if !options.Force {
|
||||
_, created, err := secretStore.GetOrCreateSecret("encryptionconfig", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding encryptionconfig secret: %v", err)
|
||||
return fmt.Errorf("adding encryptionconfig secret: %v", err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("failed to create the encryptionconfig secret as it already exists. The `--force` flag can be passed to replace an existing secret.")
|
||||
return fmt.Errorf("failed to create the encryptionconfig secret as it already exists. Pass the `--force` flag to replace an existing secret")
|
||||
}
|
||||
} else {
|
||||
_, err := secretStore.ReplaceSecret("encryptionconfig", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating encryptionconfig secret: %v", err)
|
||||
return fmt.Errorf("updating encryptionconfig secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"io/ioutil"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kops/pkg/commands/commandutils"
|
||||
|
||||
"k8s.io/kops/cmd/kops/util"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
|
|
@ -31,74 +32,66 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
createSecretWeaveEncryptionconfigLong = templates.LongDesc(i18n.T(`
|
||||
Create a new weave encryption secret, and store it in the state store.
|
||||
Used to weave networking to use encrypted communication between nodes.
|
||||
createSecretWeavePasswordLong = templates.LongDesc(i18n.T(`
|
||||
Create a new weave encryption secret and store it in the state store.
|
||||
Used by Weave networking to encrypt communication between nodes.
|
||||
|
||||
If no password is provided, kOps will generate one at random.
|
||||
|
||||
WARNING: cannot be enabled on a running cluster without downtime.`))
|
||||
WARNING: cannot be enabled or changed on a running cluster without downtime.`))
|
||||
|
||||
createSecretWeaveEncryptionconfigExample = templates.Examples(i18n.T(`
|
||||
createSecretWeavePasswordExample = templates.Examples(i18n.T(`
|
||||
# Create a new random weave password.
|
||||
kops create secret weavepassword \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Install a specific weave password.
|
||||
kops create secret weavepassword -f /path/to/weavepassword \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Install a specific weave password via stdin.
|
||||
kops create secret weavepassword -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
# Replace an existing weavepassword secret.
|
||||
|
||||
# Replace an existing weave password.
|
||||
kops create secret weavepassword -f /path/to/weavepassword --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
`))
|
||||
|
||||
createSecretWeaveEncryptionconfigShort = i18n.T(`Create a weave encryption config.`)
|
||||
createSecretWeavePasswordShort = i18n.T(`Create a Weave password.`)
|
||||
)
|
||||
|
||||
type CreateSecretWeaveEncryptionConfigOptions struct {
|
||||
type CreateSecretWeavePasswordOptions struct {
|
||||
ClusterName string
|
||||
WeavePasswordFilePath string
|
||||
Force bool
|
||||
}
|
||||
|
||||
func NewCmdCreateSecretWeaveEncryptionConfig(f *util.Factory, out io.Writer) *cobra.Command {
|
||||
options := &CreateSecretWeaveEncryptionConfigOptions{}
|
||||
func NewCmdCreateSecretWeavePassword(f *util.Factory, out io.Writer) *cobra.Command {
|
||||
options := &CreateSecretWeavePasswordOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "weavepassword",
|
||||
Short: createSecretWeaveEncryptionconfigShort,
|
||||
Long: createSecretWeaveEncryptionconfigLong,
|
||||
Example: createSecretWeaveEncryptionconfigExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.TODO()
|
||||
|
||||
err := rootCommand.ProcessArgs(args[0:])
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
|
||||
options.ClusterName = rootCommand.ClusterName(true)
|
||||
|
||||
err = RunCreateSecretWeaveEncryptionConfig(ctx, f, options)
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
Use: "weavepassword [CLUSTER]",
|
||||
Short: createSecretWeavePasswordShort,
|
||||
Long: createSecretWeavePasswordLong,
|
||||
Example: createSecretWeavePasswordExample,
|
||||
Args: rootCommand.clusterNameArgs(&options.ClusterName),
|
||||
ValidArgsFunction: commandutils.CompleteClusterName(&rootCommand, true, false),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunCreateSecretWeavePassword(context.TODO(), f, out, options)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.WeavePasswordFilePath, "", "f", "", "Path to the weave password file (optional)")
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the kOps secret if it already exists")
|
||||
cmd.Flags().StringVarP(&options.WeavePasswordFilePath, "filename", "f", "", "Path to Weave password file")
|
||||
cmd.Flags().BoolVar(&options.Force, "force", options.Force, "Force replace the secret if it already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RunCreateSecretWeaveEncryptionConfig(ctx context.Context, f *util.Factory, options *CreateSecretWeaveEncryptionConfigOptions) error {
|
||||
|
||||
func RunCreateSecretWeavePassword(ctx context.Context, f *util.Factory, out io.Writer, options *CreateSecretWeavePasswordOptions) error {
|
||||
secret, err := fi.CreateSecret()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating encryption secret: %v", err)
|
||||
return fmt.Errorf("creating Weave password: %v", err)
|
||||
}
|
||||
|
||||
cluster, err := GetCluster(ctx, f, options.ClusterName)
|
||||
|
|
@ -121,14 +114,13 @@ func RunCreateSecretWeaveEncryptionConfig(ctx context.Context, f *util.Factory,
|
|||
if options.WeavePasswordFilePath == "-" {
|
||||
data, err = ConsumeStdin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading weave password file from stdin: %v", err)
|
||||
return fmt.Errorf("reading Weave password file from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(options.WeavePasswordFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading weave password file %v: %v", options.WeavePasswordFilePath, err)
|
||||
return fmt.Errorf("reading Weave password file %v: %v", options.WeavePasswordFilePath, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
secret.Data = data
|
||||
|
|
@ -137,15 +129,15 @@ func RunCreateSecretWeaveEncryptionConfig(ctx context.Context, f *util.Factory,
|
|||
if !options.Force {
|
||||
_, created, err := secretStore.GetOrCreateSecret("weavepassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding weavepassword secret: %v", err)
|
||||
return fmt.Errorf("adding weavepassword secret: %v", err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("failed to create the weavepassword secret as it already exists. The `--force` flag can be passed to replace an existing secret")
|
||||
return fmt.Errorf("failed to create the weavepassword secret as it already exists. Pass the `--force` flag to replace an existing secret")
|
||||
}
|
||||
} else {
|
||||
_, err := secretStore.ReplaceSecret("weavepassword", secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating weavepassword secret: %v", err)
|
||||
return fmt.Errorf("updating weavepassword secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,8 +35,8 @@ Create a secret.
|
|||
### SEE ALSO
|
||||
|
||||
* [kops create](kops_create.md) - Create a resource by command line, filename or stdin.
|
||||
* [kops create secret ciliumpassword](kops_create_secret_ciliumpassword.md) - Create a cilium encryption key.
|
||||
* [kops create secret dockerconfig](kops_create_secret_dockerconfig.md) - Create a docker config.
|
||||
* [kops create secret ciliumpassword](kops_create_secret_ciliumpassword.md) - Create a Cilium IPsec configuration.
|
||||
* [kops create secret dockerconfig](kops_create_secret_dockerconfig.md) - Create a Docker config.
|
||||
* [kops create secret encryptionconfig](kops_create_secret_encryptionconfig.md) - Create an encryption config.
|
||||
* [kops create secret weavepassword](kops_create_secret_weavepassword.md) - Create a weave encryption config.
|
||||
* [kops create secret weavepassword](kops_create_secret_weavepassword.md) - Create a Weave password.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,37 +3,39 @@
|
|||
|
||||
## kops create secret ciliumpassword
|
||||
|
||||
Create a cilium encryption key.
|
||||
Create a Cilium IPsec configuration.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Create a new cilium encryption secret, and store it in the state store. Used by Cilium to generate encrypted communication between pods/nodes.
|
||||
Create a new Cilium IPsec configuration and store it in the state store. This is used by Cilium to encrypt communication between pods/nodes.
|
||||
|
||||
```
|
||||
kops create secret ciliumpassword [flags]
|
||||
kops create secret ciliumpassword [CLUSTER] -f FILENAME [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Create a new cilium encryption key.
|
||||
kops create secret ciliumpassword -f /path/to/ciliumpassword \
|
||||
# Create a new Cilium IPsec configuration.
|
||||
kops create secret ciliumpassword -f /path/to/configuration.yaml \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
# Create a new cilium encryption key via stdin.
|
||||
|
||||
# Create a new Cilium IPsec key via stdin.
|
||||
cat <<EOF | kops create secret ciliumpassword --name k8s-cluster.example.com --state s3://my-state-store -f -
|
||||
keys: $(echo "3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2> /dev/null| xxd -p -c 64)) 128")
|
||||
EOF
|
||||
# Replace an existing ciliumpassword secret
|
||||
kops create secret ciliumpassword -f /path/to/ciliumpassword --force \
|
||||
|
||||
# Replace an existing Cilium IPsec configuration secret
|
||||
kops create secret ciliumpassword -f /path/to/configuration.yaml --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-f, -- string Path to the cilium encryption config file
|
||||
--force Force replace the kOps secret if it already exists
|
||||
-h, --help help for ciliumpassword
|
||||
-f, --filename string Path to the Cilium IPsec configuration file
|
||||
--force Force replace the secret if it already exists
|
||||
-h, --help help for ciliumpassword
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
|||
|
|
@ -3,27 +3,31 @@
|
|||
|
||||
## kops create secret dockerconfig
|
||||
|
||||
Create a docker config.
|
||||
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 (i.e. for auth) Use update to modify it, this command will only create a new entry.
|
||||
Create a new Docker config and store it in the state store. Used to configure Docker authentication on each node.
|
||||
|
||||
After creating a dockerconfig secret, a /root/.docker/config.json file will be added to newly created nodes. This file will be used by Kubernetes to authenticate to container registries and will also work when using containerd as container runtime.
|
||||
After creating a dockerconfig secret a /root/.docker/config.json file will be added to newly created nodes. This file will be used by Kubernetes to authenticate to container registries.
|
||||
|
||||
This will also work when using containerd as the container runtime.
|
||||
|
||||
```
|
||||
kops create secret dockerconfig [flags]
|
||||
kops create secret dockerconfig [CLUSTER] -f FILENAME [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Create a new docker config.
|
||||
# Create a new Docker config.
|
||||
kops create secret dockerconfig -f /path/to/docker/config.json \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Create a docker config via stdin.
|
||||
generate-docker-config.sh | kops create secret dockerconfig -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Replace an existing docker config secret.
|
||||
kops create secret dockerconfig -f /path/to/docker/config.json --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
|
@ -32,9 +36,9 @@ kops create secret dockerconfig [flags]
|
|||
### Options
|
||||
|
||||
```
|
||||
-f, -- string Path to docker config JSON file
|
||||
--force Force replace the kOps secret if it already exists
|
||||
-h, --help help for dockerconfig
|
||||
-f, --filename string Path to Docker config JSON file
|
||||
--force Force replace the secret if it already exists
|
||||
-h, --help help for dockerconfig
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ Create an encryption config.
|
|||
|
||||
### Synopsis
|
||||
|
||||
Create a new encryption config, and store it in the state store. Used to configure encryption-at-rest by the kube-apiserver process on each of the master nodes. The config is not updated by this command.
|
||||
Create a new encryption config and store it in the state store. Used to configure encryption-at-rest by the kube-apiserver process.
|
||||
|
||||
```
|
||||
kops create secret encryptionconfig [flags]
|
||||
kops create secret encryptionconfig [CLUSTER] -f FILENAME [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
|
@ -19,9 +19,11 @@ kops create secret encryptionconfig [flags]
|
|||
# Create a new encryption config.
|
||||
kops create secret encryptionconfig -f config.yaml \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Create a new encryption config via stdin.
|
||||
generate-encryption-config.sh | kops create secret encryptionconfig -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Replace an existing encryption config secret.
|
||||
kops create secret encryptionconfig -f config.yaml --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
|
@ -30,9 +32,9 @@ kops create secret encryptionconfig [flags]
|
|||
### Options
|
||||
|
||||
```
|
||||
-f, -- string Path to encryption config yaml file
|
||||
--force Force replace the kOps secret if it already exists
|
||||
-h, --help help for encryptionconfig
|
||||
-f, --filename string Path to encryption config YAML file
|
||||
--force Force replace the secret if it already exists
|
||||
-h, --help help for encryptionconfig
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
|
||||
## kops create secret weavepassword
|
||||
|
||||
Create a weave encryption config.
|
||||
Create a Weave password.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Create a new weave encryption secret, and store it in the state store. Used to weave networking to use encrypted communication between nodes.
|
||||
Create a new weave encryption secret and store it in the state store. Used by Weave networking to encrypt communication between nodes.
|
||||
|
||||
If no password is provided, kOps will generate one at random.
|
||||
|
||||
WARNING: cannot be enabled on a running cluster without downtime.
|
||||
WARNING: cannot be enabled or changed on a running cluster without downtime.
|
||||
|
||||
```
|
||||
kops create secret weavepassword [flags]
|
||||
kops create secret weavepassword [CLUSTER] [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
|
@ -23,13 +23,16 @@ kops create secret weavepassword [flags]
|
|||
# Create a new random weave password.
|
||||
kops create secret weavepassword \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Install a specific weave password.
|
||||
kops create secret weavepassword -f /path/to/weavepassword \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
|
||||
# Install a specific weave password via stdin.
|
||||
kops create secret weavepassword -f - \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
# Replace an existing weavepassword secret.
|
||||
|
||||
# Replace an existing weave password.
|
||||
kops create secret weavepassword -f /path/to/weavepassword --force \
|
||||
--name k8s-cluster.example.com --state s3://my-state-store
|
||||
```
|
||||
|
|
@ -37,9 +40,9 @@ kops create secret weavepassword [flags]
|
|||
### Options
|
||||
|
||||
```
|
||||
-f, -- string Path to the weave password file (optional)
|
||||
--force Force replace the kOps secret if it already exists
|
||||
-h, --help help for weavepassword
|
||||
-f, --filename string Path to Weave password file
|
||||
--force Force replace the secret if it already exists
|
||||
-h, --help help for weavepassword
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
|||
Loading…
Reference in New Issue