diff --git a/.github/workflows/build-client.yaml b/.github/workflows/build-client.yaml index bffbff0..dd95903 100644 --- a/.github/workflows/build-client.yaml +++ b/.github/workflows/build-client.yaml @@ -7,19 +7,22 @@ on: branches: [ main ] jobs: build_and_test: + name: Unit testing and linting + runs-on: ubuntu-latest env: CGO_ENABLED: 0 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: github.event_name == 'pull_request' with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: github.event_name != 'pull_request' - - name: Set up Go 1.19 - uses: actions/setup-go@v1 + - name: Set up Go + uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version-file: 'go.mod' + cache: true - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: @@ -39,18 +42,11 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} @@ -61,7 +57,7 @@ jobs: echo ::set-output name=tag_name::${GITHUB_REF#refs/tags/} - name: Build and push id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: file: cmd/kubectl-k8ssandra/Dockerfile push: ${{ github.event_name != 'pull_request' }} diff --git a/cmd/kubectl-k8ssandra/Dockerfile b/cmd/kubectl-k8ssandra/Dockerfile index 53bee85..892f536 100644 --- a/cmd/kubectl-k8ssandra/Dockerfile +++ b/cmd/kubectl-k8ssandra/Dockerfile @@ -13,7 +13,7 @@ COPY cmd/ cmd/ COPY pkg/ pkg/ # Build -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o kubectl-k8ssandra cmd/kubectl-k8ssandra/main.go +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o kubectl-k8ssandra cmd/kubectl-k8ssandra/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go b/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go index c27116f..45d9db4 100644 --- a/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go +++ b/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go @@ -9,7 +9,7 @@ import ( // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/migrate" // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/nodetool" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/operate" - // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/users" + "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/users" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -47,7 +47,7 @@ func NewCmd(streams genericclioptions.IOStreams) *cobra.Command { cmd.AddCommand(operate.NewStopCmd(streams)) // cmd.AddCommand(list.NewCmd(streams)) // cmd.AddCommand(migrate.NewCmd(streams)) - // cmd.AddCommand(users.NewCmd(streams)) + cmd.AddCommand(users.NewCmd(streams)) // cmd.AddCommand(migrate.NewInstallCmd(streams)) // cmd.Flags().BoolVar(&o.listNamespaces, "list", o.listNamespaces, "if true, print the list of all namespaces in the current KUBECONFIG") diff --git a/cmd/kubectl-k8ssandra/users/add.go b/cmd/kubectl-k8ssandra/users/add.go new file mode 100644 index 0000000..10994ef --- /dev/null +++ b/cmd/kubectl-k8ssandra/users/add.go @@ -0,0 +1,159 @@ +package users + +import ( + "context" + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/k8ssandra/k8ssandra-client/pkg/kubernetes" + "github.com/k8ssandra/k8ssandra-client/pkg/ui" + "github.com/k8ssandra/k8ssandra-client/pkg/users" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var ( + userAddExample = ` + # Add new users to CassandraDatacenter + %[1]s add [] + + # Add new superusers to CassandraDatacenter dc1 from a path /tmp/users.txt + %[1]s add --dc dc1 --path /tmp/users.txt --superuser + ` + errNoDcDc = fmt.Errorf("target CassandraDatacenter is required") + errDoubleDefinition = fmt.Errorf("either --path or --username is allowed, not both") + errMissingUsername = fmt.Errorf("if --password is set, --username is required") +) + +type addOptions struct { + configFlags *genericclioptions.ConfigFlags + genericclioptions.IOStreams + namespace string + datacenter string + superuser bool + + // For manual entering from CLI + username string + password string + + // When reading from files + secretPath string +} + +func newAddOptions(streams genericclioptions.IOStreams) *addOptions { + return &addOptions{ + configFlags: genericclioptions.NewConfigFlags(true), + IOStreams: streams, + } +} + +// NewCmd provides a cobra command wrapping newAddOptions +func NewAddCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := newAddOptions(streams) + + cmd := &cobra.Command{ + Use: "add [flags]", + Short: "Add new users to CassandraDatacenter installation", + Example: fmt.Sprintf(userAddExample, "kubectl k8ssandra users"), + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(c, args); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + + return nil + }, + } + + fl := cmd.Flags() + fl.StringVar(&o.secretPath, "path", "", "path to users data") + fl.StringVar(&o.datacenter, "dc", "", "target datacenter") + fl.BoolVar(&o.superuser, "superuser", true, "create users as superusers") + fl.StringVarP(&o.username, "username", "u", "", "username to add") + fl.StringVarP(&o.password, "password", "p", "", "password to set for the user") + o.configFlags.AddFlags(fl) + return cmd +} + +// Complete parses the arguments and necessary flags to options +func (c *addOptions) Complete(cmd *cobra.Command, args []string) error { + var err error + + c.namespace, _, err = c.configFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + return nil +} + +// Validate ensures that all required arguments and flag values are provided +func (c *addOptions) Validate() error { + if c.datacenter == "" { + return errNoDcDc + } + + if c.secretPath != "" && c.username != "" { + return errDoubleDefinition + } + + if c.password != "" && c.username == "" { + return errMissingUsername + } + + return nil +} + +// Run processes the input, creates a connection to Kubernetes and processes a secret to add the users +func (c *addOptions) Run() error { + restConfig, err := c.configFlags.ToRESTConfig() + if err != nil { + return err + } + + kubeClient, err := kubernetes.GetClientInNamespace(restConfig, c.namespace) + if err != nil { + return err + } + + ctx := context.Background() + + if c.secretPath != "" { + return users.AddNewUsersFromSecret(ctx, kubeClient, c.datacenter, c.secretPath, c.superuser) + } + + // Interactive prompt + + prompts := make([]*ui.Prompt, 0, 1) + + userPrompt := ui.NewPrompt("Username") + passPrompt := ui.NewPrompt("Password").Mask() + + if c.username == "" { + prompts = append(prompts, userPrompt) + } + + if c.password == "" { + prompts = append(prompts, passPrompt) + } + + if len(prompts) > 0 { + prompter := ui.NewPrompter(prompts) + if _, err := tea.NewProgram(prompter).Run(); err != nil { + return err + } + + // Parse values + c.password = passPrompt.Value() + if c.username == "" { + c.username = userPrompt.Value() + } + } + + return users.AddNewUser(ctx, kubeClient, c.datacenter, c.username, c.password, c.superuser) +} diff --git a/cmd/kubectl-k8ssandra/users/users.go b/cmd/kubectl-k8ssandra/users/users.go new file mode 100644 index 0000000..e673600 --- /dev/null +++ b/cmd/kubectl-k8ssandra/users/users.go @@ -0,0 +1,34 @@ +package users + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +type ClientOptions struct { + configFlags *genericclioptions.ConfigFlags + genericclioptions.IOStreams +} + +// NewClientOptions provides an instance of ClientOptions with default values +func NewClientOptions(streams genericclioptions.IOStreams) *ClientOptions { + return &ClientOptions{ + configFlags: genericclioptions.NewConfigFlags(true), + IOStreams: streams, + } +} + +// NewCmd provides a cobra command wrapping ClientOptions +func NewCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := NewClientOptions(streams) + + cmd := &cobra.Command{ + Use: "users [subcommand] [flags]", + } + + // Add subcommands + cmd.AddCommand(NewAddCmd(streams)) + o.configFlags.AddFlags(cmd.Flags()) + + return cmd +} diff --git a/go.mod b/go.mod index 36ac76f..778d1bf 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,14 @@ module github.com/k8ssandra/k8ssandra-client go 1.19 require ( + github.com/charmbracelet/bubbles v0.14.0 + github.com/charmbracelet/bubbletea v0.23.1 + github.com/charmbracelet/lipgloss v0.5.0 + github.com/google/uuid v1.2.0 github.com/k8ssandra/cass-operator v1.13.1 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.7.2 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.9.4 k8s.io/api v0.24.2 @@ -35,9 +40,12 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/containerd/containerd v1.6.6 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -71,7 +79,6 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.2.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect @@ -86,10 +93,12 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.6 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect @@ -101,6 +110,10 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.13.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect @@ -111,12 +124,12 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/rubenv/sql-migrate v1.1.1 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/cast v1.4.1 // indirect - github.com/stretchr/testify v1.7.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect @@ -126,7 +139,7 @@ require ( golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect diff --git a/go.sum b/go.sum index d2e1e6e..2d3adcc 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -134,6 +138,14 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= +github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= +github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck= +github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -150,6 +162,8 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -467,6 +481,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -478,6 +493,8 @@ github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -497,12 +514,19 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -546,6 +570,18 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -621,6 +657,9 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -632,6 +671,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -962,9 +1002,10 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index e85594c..083a98f 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -30,14 +30,14 @@ func GetClient(restConfig *rest.Config) (client.Client, error) { return c, err } -func GetClientInNamespace(restConfig *rest.Config, namespace string) (*NamespacedClient, error) { +func GetClientInNamespace(restConfig *rest.Config, namespace string) (NamespacedClient, error) { c, err := GetClient(restConfig) if err != nil { - return nil, err + return NamespacedClient{}, err } c = client.NewNamespacedClient(c, namespace) - return &NamespacedClient{ + return NamespacedClient{ config: restConfig, Client: c, }, nil diff --git a/pkg/httphelper/client.go b/pkg/mgmtapi/client.go similarity index 98% rename from pkg/httphelper/client.go rename to pkg/mgmtapi/client.go index c6c3a04..f73d7e0 100644 --- a/pkg/httphelper/client.go +++ b/pkg/mgmtapi/client.go @@ -1,4 +1,4 @@ -package migrate +package mgmtapi import ( "context" diff --git a/pkg/secrets/mount.go b/pkg/secrets/mount.go new file mode 100644 index 0000000..dfd0e91 --- /dev/null +++ b/pkg/secrets/mount.go @@ -0,0 +1,95 @@ +package secrets + +import ( + "bufio" + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// readTargetPath supports two different formats for users. If the target is a file, it must be in the format +// username=password, if it's a directory, then it must follow the Kubernetes secret format, +// filename = username, file = password +func ReadTargetPath(path string) (map[string]string, error) { + f, err := os.Stat(path) + if err != nil { + return nil, err + } + + if f.IsDir() { + return readTargetSecretMount(path) + } + return readTargetFile(path) +} + +// readTargetSecretMount is processing the old standard set by cass-operator +// this method can only parse a single username/password pair +func readTargetSecretMount(path string) (map[string]string, error) { + var username, password string + + err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { + if err != nil { + // Couldn't access the file for some reason + return err + } + + if d.IsDir() { + // This will be walked later + return nil + } + + // We should have two keys here: username and password and use that information.. + f, err := os.Open(path) + if err != nil { + return err + } + + defer f.Close() + + fileContents, err := io.ReadAll(f) + if err != nil { + return err + } + data := string(fileContents) + + switch d.Name() { + case "password": + password = data + case "username": + username = data + } + + return nil + }) + + return map[string]string{username: password}, err +} + +func readTargetFile(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + defer f.Close() + + users := make(map[string]string) + + // Remove the comment lines to reduce the ConfigMap size + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + userInfo := strings.SplitN(line, "=", 2) + if len(userInfo) > 1 { + users[userInfo[0]] = userInfo[1] + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return users, nil +} diff --git a/pkg/secrets/mount_test.go b/pkg/secrets/mount_test.go new file mode 100644 index 0000000..db56d62 --- /dev/null +++ b/pkg/secrets/mount_test.go @@ -0,0 +1,59 @@ +package secrets + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVaultMounted(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "secret-") + require.NoError(err) + + tmpFile, err := os.CreateTemp(tmpDir, "users") + require.NoError(err) + + defer func() { + tmpFile.Close() + os.RemoveAll(tmpDir) + }() + + _, err = tmpFile.WriteString("newuser=password====") + require.NoError(err) + + users, err := readTargetFile(tmpFile.Name()) + require.NoError(err) + require.Equal(1, len(users)) + require.Contains(users, "newuser") + require.Equal("password====", users["newuser"]) +} + +func TestSecretMounted(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "secret-") + require.NoError(err) + + username := "superuser" + password := "superpassword" + + err = os.WriteFile(filepath.Join(tmpDir, "username"), []byte(username), 0644) + require.NoError(err) + + err = os.WriteFile(filepath.Join(tmpDir, "password"), []byte(password), 0644) + require.NoError(err) + + defer func() { + os.RemoveAll(tmpDir) + }() + + users, err := readTargetSecretMount(tmpDir) + require.NoError(err) + require.Equal(1, len(users)) + require.Contains(users, username) + require.Equal(password, users[username]) +} diff --git a/pkg/ui/prompt.go b/pkg/ui/prompt.go new file mode 100644 index 0000000..bfccb71 --- /dev/null +++ b/pkg/ui/prompt.go @@ -0,0 +1,143 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type Prompter struct { + currentPos int + inputs []textinput.Model + prompts []*Prompt +} + +func NewPrompter(prompts []*Prompt) *Prompter { + textinputs := make([]textinput.Model, len(prompts)) + for i := range prompts { + ti := textinput.New() + ti.Prompt = prompts[i].text + + // Set default styles here etc + if prompts[i].maskedValue { + ti.EchoCharacter = '*' + ti.EchoMode = textinput.EchoPassword + } + + ti = applyDefaultLayout(ti) + textinputs[i] = ti + } + + textinputs[0].Focus() + + return &Prompter{ + inputs: textinputs, + prompts: prompts, + currentPos: 0, + } +} + +func applyDefaultLayout(ti textinput.Model) textinput.Model { + promptStyle := lipgloss.NewStyle().Width(15) + + ti.PromptStyle = promptStyle + ti.CharLimit = 32 + return ti +} + +func (p *Prompter) Init() tea.Cmd { + return textinput.Blink +} + +type errMsg error + +func (p *Prompter) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return p, tea.Quit + case tea.KeyEnter, tea.KeyTab: + // If this is the last one, quit + if p.currentPos == len(p.inputs)-1 { + p.updateValues() + return p, tea.Quit + } + + p.nextInput() + + for i := range p.inputs { + p.inputs[i].Blur() + } + p.inputs[p.currentPos].Focus() + } + + // We handle errors just like any other message + case errMsg: + // p.err = msg + return p, nil + } + + // Handle character input and blinking + return p, p.updateInputs(msg) +} + +func (p *Prompter) updateValues() { + for i := range p.inputs { + p.prompts[i].rValue = p.inputs[i].Value() + } +} + +func (p *Prompter) nextInput() { + p.currentPos = (p.currentPos + 1) % len(p.inputs) +} + +func (p *Prompter) View() string { + var sb strings.Builder + for i := range p.inputs { + if i > p.currentPos { + break + } + sb.WriteString(p.inputs[i].View()) + if i < len(p.inputs) { + sb.WriteRune('\n') + } + } + + return sb.String() +} + +func (p *Prompter) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(p.inputs)) + + for i := range p.inputs { + p.inputs[i], cmds[i] = p.inputs[i].Update(msg) + } + + return tea.Batch(cmds...) +} + +type Prompt struct { + text string + rValue string + maskedValue bool +} + +func NewPrompt(text string) *Prompt { + return &Prompt{ + text: text, + } +} + +func (p *Prompt) Value() string { + return p.rValue +} + +func (p *Prompt) Mask() *Prompt { + p.maskedValue = true + return p +} + +// TODO Add ValidationFunc etc diff --git a/pkg/users/users.go b/pkg/users/users.go new file mode 100644 index 0000000..137b001 --- /dev/null +++ b/pkg/users/users.go @@ -0,0 +1,71 @@ +package users + +import ( + "context" + + "github.com/k8ssandra/k8ssandra-client/pkg/cassdcutil" + "github.com/k8ssandra/k8ssandra-client/pkg/kubernetes" + "github.com/k8ssandra/k8ssandra-client/pkg/mgmtapi" + "github.com/k8ssandra/k8ssandra-client/pkg/secrets" + + corev1 "k8s.io/api/core/v1" +) + +func AddNewUsersFromSecret(ctx context.Context, c kubernetes.NamespacedClient, datacenter string, secretPath string, superusers bool) error { + // Create ManagementClient + mgmtClient, err := mgmtapi.NewManagementClient(ctx, c) + if err != nil { + return err + } + + pod, err := targetPod(ctx, c, datacenter) + if err != nil { + return err + } + + users, err := secrets.ReadTargetPath(secretPath) + if err != nil { + return err + } + + for user, pass := range users { + if err := mgmtClient.CallCreateRoleEndpoint(pod, user, pass, superusers); err != nil { + return err + } + } + + return nil +} + +func targetPod(ctx context.Context, c kubernetes.NamespacedClient, datacenter string) (*corev1.Pod, error) { + cassManager := cassdcutil.NewManager(c) + dc, err := cassManager.CassandraDatacenter(ctx, datacenter, c.Namespace) + if err != nil { + return nil, err + } + + podList, err := cassManager.CassandraDatacenterPods(ctx, dc) + if err != nil { + return nil, err + } + + return &podList.Items[0], nil +} + +func AddNewUser(ctx context.Context, c kubernetes.NamespacedClient, datacenter string, username string, password string, superuser bool) error { + mgmtClient, err := mgmtapi.NewManagementClient(ctx, c) + if err != nil { + return err + } + + pod, err := targetPod(ctx, c, datacenter) + if err != nil { + return err + } + + if err := mgmtClient.CallCreateRoleEndpoint(pod, username, password, superuser); err != nil { + return err + } + + return nil +}