From 848b745f88446583bafc8379e8256a2da77995cf Mon Sep 17 00:00:00 2001 From: Joshua Reese Date: Sun, 29 Dec 2024 14:54:56 -0600 Subject: [PATCH 1/4] feat: Add the ability to list organizations for the authenticated user, tweaked flag checks on update-kubeconfig. --- internal/cmd/auth/update-kubeconfig.go | 7 +- internal/cmd/organizations/list.go | 88 +++++++++++++++++++++ internal/cmd/organizations/organizations.go | 16 ++++ internal/cmd/root.go | 7 +- 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 internal/cmd/organizations/list.go create mode 100644 internal/cmd/organizations/organizations.go diff --git a/internal/cmd/auth/update-kubeconfig.go b/internal/cmd/auth/update-kubeconfig.go index 818763c..a0c7fe5 100644 --- a/internal/cmd/auth/update-kubeconfig.go +++ b/internal/cmd/auth/update-kubeconfig.go @@ -33,10 +33,6 @@ func updateKubeconfigCmd() *cobra.Command { return fmt.Errorf("failed to parse base URL option: %w", err) } - if organizationName == "" && projectName == "" { - return errors.New("the `--organization` or `--project` flag is required") - } - if projectName != "" { serverURL.Path = "/apis/resourcemanager.datumapis.com/v1alpha/projects/" + projectName + "/control-plane" } else { @@ -92,6 +88,9 @@ func updateKubeconfigCmd() *cobra.Command { cmd.Flags().StringVar(&baseURL, "base-url", "https://api.datum.net", "The base URL of the Datum Cloud API") cmd.Flags().StringVar(&projectName, "project", "", "Configure kubectl to access a specific project's control plane instead of the core control plane.") cmd.Flags().StringVar(&organizationName, "organization", "", "The organization name that is being connected to.") + + cmd.MarkFlagsOneRequired("project", "organization") + cmd.MarkFlagsMutuallyExclusive("project", "organization") return cmd } diff --git a/internal/cmd/organizations/list.go b/internal/cmd/organizations/list.go new file mode 100644 index 0000000..7532c04 --- /dev/null +++ b/internal/cmd/organizations/list.go @@ -0,0 +1,88 @@ +package organizations + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/spf13/cobra" + + "go.datum.net/datumctl/internal/keyring" +) + +type listResponse struct { + Data struct { + Organizations struct { + Edges []struct { + Node struct { + Name string `json:"name"` + UserEntityID string `json:"userEntityID"` + } `json:"node"` + } `json:"edges"` + } `json:"organizations"` + } `json:"data"` +} + +func listOrgsCommand() *cobra.Command { + var hostname string + + cmd := &cobra.Command{ + Use: "list", + Short: "List organizations for the authenticated user", + RunE: func(cmd *cobra.Command, _ []string) error { + token, err := keyring.Get("datumctl", "datumctl") + if err != nil { + return fmt.Errorf("failed to get token from keyring: %w", err) + } + + url := fmt.Sprintf("https://%s/query", hostname) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(getAllOrganizationsRequest)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + client := http.DefaultClient + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unexpected status code %d from token endpoint", resp.StatusCode) + } + + var r listResponse + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + return fmt.Errorf("failed to decode JSON response: %w", err) + } + + fmt.Printf("%-20s\t%-20s\n", "NAME", "RESOURCE ID") + + if len(r.Data.Organizations.Edges) == 0 { + fmt.Printf("No organizations found") + } else { + for _, org := range r.Data.Organizations.Edges { + fmt.Printf("%-20s\t%-20s\n", org.Node.Name, org.Node.UserEntityID) + } + } + + return nil + }, + } + + cmd.Flags().StringVar(&hostname, "hostname", "api.datum.net", "The hostname of the Datum Cloud instance to authenticate with") + + return cmd +} + +const getAllOrganizationsRequest = `{ + "operationName": "GetAllOrganizations", + "query": "query GetAllOrganizations {organizations {edges {node {name userEntityID}}}}" +}` diff --git a/internal/cmd/organizations/organizations.go b/internal/cmd/organizations/organizations.go new file mode 100644 index 0000000..5ac0866 --- /dev/null +++ b/internal/cmd/organizations/organizations.go @@ -0,0 +1,16 @@ +package organizations + +import ( + "github.com/spf13/cobra" +) + +var Command = &cobra.Command{ + Use: "organizations", + Short: "Manage organizations", +} + +func init() { + Command.AddCommand( + listOrgsCommand(), + ) +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 7c028bc..865d7e3 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -2,7 +2,9 @@ package cmd import ( "github.com/spf13/cobra" + "go.datum.net/datumctl/internal/cmd/auth" + "go.datum.net/datumctl/internal/cmd/organizations" ) var rootCmd = &cobra.Command{ @@ -11,7 +13,10 @@ var rootCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(auth.Command) + rootCmd.AddCommand( + auth.Command, + organizations.Command, + ) } func Execute() error { From f27b0e544f57ba6ca0ded13dfcd95e94ab8a32db Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Sun, 29 Dec 2024 22:23:23 +0000 Subject: [PATCH 2/4] feat: support table, json, and yaml output formats --- go.mod | 19 ++++- go.sum | 61 ++++++++++++-- internal/cmd/auth/auth.go | 14 ++-- internal/cmd/organizations/list.go | 83 +++++++++--------- internal/cmd/organizations/organizations.go | 14 ++-- internal/cmd/root.go | 4 +- internal/resourcemanager/organizations.go | 93 +++++++++++++++++++++ 7 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 internal/resourcemanager/organizations.go diff --git a/go.mod b/go.mod index 1dd51df..3d7bd45 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,32 @@ module go.datum.net/datumctl go 1.23.1 require ( + buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go v1.36.1-20241220153950-69c59230c8e9.1 + buf.build/go/protoyaml v0.3.1 + github.com/rodaine/table v1.3.0 github.com/spf13/cobra v1.8.1 github.com/zalando/go-keyring v0.2.6 golang.org/x/oauth2 v0.24.0 golang.org/x/term v0.27.0 + google.golang.org/protobuf v1.36.1 k8s.io/apimachinery v0.32.0 k8s.io/client-go v0.32.0 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 // indirect + cel.dev/expr v0.18.0 // indirect + cloud.google.com/go/longrunning v0.6.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/bufbuild/protovalidate-go v0.8.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/cel-go v0.22.1 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -26,12 +36,19 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.32.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect + google.golang.org/grpc v1.67.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/go.sum b/go.sum index e519e19..8d4e708 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,19 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 h1:ntAj16eF7AtUyzOOAFk5gvbAO52QmUKPKk7GmsIEORo= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1/go.mod h1:AxRT+qTj5PJCz2nyQzsR/qxAcveW5USRhJTt/edTO5w= +buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go v1.36.1-20241220153950-69c59230c8e9.1 h1:IivlDutpvF9fSxITQHCKttJzEiZ5WbeyCNK9O/J01fk= +buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go v1.36.1-20241220153950-69c59230c8e9.1/go.mod h1:oJZ1dK0lOxvkFEYPFqF6fiead/Sm7EXOaajXAgu8L+A= +buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4= +buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg= +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= +cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/bufbuild/protovalidate-go v0.8.0 h1:Xs3kCLCJ4tQiogJ0iOXm+ClKw/KviW3nLAryCGW2I3Y= +github.com/bufbuild/protovalidate-go v0.8.0/go.mod h1:JPWZInGm2y2NBg3vKDKdDIkvDjyLv31J3hLH5GIFc/Q= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= @@ -9,6 +23,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -25,6 +41,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= +github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -45,8 +63,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -59,17 +83,30 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= +github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -79,14 +116,16 @@ github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -113,14 +152,24 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8 h1:e26eS1K69yxjjNNHYqjN49y95kcaQLJ3TL5h68dcA1E= +google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:i5btTErZyoKCCubju3HS5LVho4nZd3yFnEp6moqeUjE= +google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= +google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go index 948974c..b7fe4b0 100644 --- a/internal/cmd/auth/auth.go +++ b/internal/cmd/auth/auth.go @@ -2,16 +2,18 @@ package auth import "github.com/spf13/cobra" -var Command = &cobra.Command{ - Use: "auth", - Short: "Authenticate with Datum Cloud", -} +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Short: "Authenticate with Datum Cloud", + } -func init() { - Command.AddCommand( + cmd.AddCommand( activateAPITokenCmd(), getTokenCmd(), logoutCmd(), updateKubeconfigCmd(), ) + + return cmd } diff --git a/internal/cmd/organizations/list.go b/internal/cmd/organizations/list.go index 7532c04..cc6940f 100644 --- a/internal/cmd/organizations/list.go +++ b/internal/cmd/organizations/list.go @@ -1,15 +1,16 @@ package organizations import ( - "context" - "encoding/json" "fmt" - "net/http" - "strings" + resourcemanagerv1alpha "buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go/datum/os/resourcemanager/v1alpha" + "buf.build/go/protoyaml" + "github.com/rodaine/table" "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" "go.datum.net/datumctl/internal/keyring" + "go.datum.net/datumctl/internal/resourcemanager" ) type listResponse struct { @@ -26,7 +27,7 @@ type listResponse struct { } func listOrgsCommand() *cobra.Command { - var hostname string + var hostname, outputFormat string cmd := &cobra.Command{ Use: "list", @@ -37,40 +38,50 @@ func listOrgsCommand() *cobra.Command { return fmt.Errorf("failed to get token from keyring: %w", err) } - url := fmt.Sprintf("https://%s/query", hostname) - - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(getAllOrganizationsRequest)) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) + organizationsAPI := &resourcemanager.OrganizationsAPI{ + PAT: token, + Hostname: hostname, } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := http.DefaultClient - resp, err := client.Do(req) + listOrgs, err := organizationsAPI.ListOrganizations(cmd.Context(), &resourcemanagerv1alpha.ListOrganizationsRequest{}) if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("unexpected status code %d from token endpoint", resp.StatusCode) - } - - var r listResponse - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - return fmt.Errorf("failed to decode JSON response: %w", err) + return fmt.Errorf("failed to list organizations: %w", err) } - fmt.Printf("%-20s\t%-20s\n", "NAME", "RESOURCE ID") - - if len(r.Data.Organizations.Edges) == 0 { - fmt.Printf("No organizations found") - } else { - for _, org := range r.Data.Organizations.Edges { - fmt.Printf("%-20s\t%-20s\n", org.Node.Name, org.Node.UserEntityID) + // TODO: We should look at abstracting the formatting here into a library + // that can be used by multiple commands needing to offer multiple + // output formats from a command. + switch outputFormat { + case "yaml": + marshaller := &protoyaml.MarshalOptions{ + Indent: 2, + EmitUnpopulated: false, + } + output, err := marshaller.Marshal(listOrgs) + if err != nil { + return fmt.Errorf("failed to list organizations: %w", err) + } + fmt.Print(string(output)) + case "json": + marshaller := &protojson.MarshalOptions{ + Indent: " ", + Multiline: true, } + output, err := marshaller.Marshal(listOrgs) + if err != nil { + return fmt.Errorf("failed to list organizations: %w", err) + } + fmt.Print(string(output)) + case "table": + orgTable := table.New("DISPLAY NAME", "RESOURCE ID") + if len(listOrgs.Organizations) == 0 { + fmt.Printf("No organizations found") + } else { + for _, org := range listOrgs.Organizations { + orgTable.AddRow(org.DisplayName, org.OrganizationId) + } + } + orgTable.Print() } return nil @@ -78,11 +89,7 @@ func listOrgsCommand() *cobra.Command { } cmd.Flags().StringVar(&hostname, "hostname", "api.datum.net", "The hostname of the Datum Cloud instance to authenticate with") + cmd.Flags().StringVar(&outputFormat, "output", "table", "Specify the output format to use. Supported options: table, json, yaml") return cmd } - -const getAllOrganizationsRequest = `{ - "operationName": "GetAllOrganizations", - "query": "query GetAllOrganizations {organizations {edges {node {name userEntityID}}}}" -}` diff --git a/internal/cmd/organizations/organizations.go b/internal/cmd/organizations/organizations.go index 5ac0866..7ae82e4 100644 --- a/internal/cmd/organizations/organizations.go +++ b/internal/cmd/organizations/organizations.go @@ -4,13 +4,15 @@ import ( "github.com/spf13/cobra" ) -var Command = &cobra.Command{ - Use: "organizations", - Short: "Manage organizations", -} +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "organizations", + Short: "Manage organizations", + } -func init() { - Command.AddCommand( + cmd.AddCommand( listOrgsCommand(), ) + + return cmd } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 865d7e3..0890154 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -14,8 +14,8 @@ var rootCmd = &cobra.Command{ func init() { rootCmd.AddCommand( - auth.Command, - organizations.Command, + auth.Command(), + organizations.Command(), ) } diff --git a/internal/resourcemanager/organizations.go b/internal/resourcemanager/organizations.go new file mode 100644 index 0000000..9e9d023 --- /dev/null +++ b/internal/resourcemanager/organizations.go @@ -0,0 +1,93 @@ +package resourcemanager + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + resourcemanagerpb "buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go/datum/os/resourcemanager/v1alpha" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type listOrganizationsGraphQLResponse struct { + Data struct { + Organizations struct { + Edges []struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + UserEntityID string `json:"userEntityID"` + CreatedAt time.Time `json:"createdAt"` + Description string `json:"description"` + } `json:"node"` + } `json:"edges"` + } `json:"organizations"` + } `json:"data"` +} + +type OrganizationsAPI struct { + // The personal access token to use when authenticating with the API. + PAT string + + // The hostname to use when connecting to the upstream API to retrieve + // organizations. + Hostname string +} + +func (r *OrganizationsAPI) ListOrganizations(ctx context.Context, req *resourcemanagerpb.ListOrganizationsRequest) (*resourcemanagerpb.ListOrganizationsResponse, error) { + httpReq, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + fmt.Sprintf("https://%s/query", r.Hostname), + strings.NewReader(getAllOrganizationsRequest), + ) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+r.PAT) + + client := http.DefaultClient + + httpResp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to make request: %w", err) + } + defer httpResp.Body.Close() + + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + payload, _ := io.ReadAll(httpResp.Body) + return nil, fmt.Errorf("unexpected status code %d from graphql endpoint: %s", httpResp.StatusCode, string(payload)) + } + + var listResp listOrganizationsGraphQLResponse + if err := json.NewDecoder(httpResp.Body).Decode(&listResp); err != nil { + return nil, fmt.Errorf("failed to decode JSON response: %w", err) + } + + resp := &resourcemanagerpb.ListOrganizationsResponse{} + for _, org := range listResp.Data.Organizations.Edges { + resp.Organizations = append(resp.Organizations, &resourcemanagerpb.Organization{ + Name: "organizations/" + org.Node.UserEntityID, + DisplayName: org.Node.Name, + Uid: org.Node.ID, + OrganizationId: org.Node.UserEntityID, + CreateTime: timestamppb.New(org.Node.CreatedAt), + Annotations: map[string]string{ + "meta.datum.net/description": org.Node.Description, + }, + }) + } + + return resp, nil +} + +const getAllOrganizationsRequest = `{ + "operationName": "GetAllOrganizations", + "query": "query GetAllOrganizations {organizations {edges {node {id name userEntityID createdAt description}}}}" +}` From 1614dc6e0245d01090e8072d63d71795ae2c04e6 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Sun, 29 Dec 2024 22:26:41 +0000 Subject: [PATCH 3/4] chore: don't indent json output --- internal/cmd/organizations/list.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/cmd/organizations/list.go b/internal/cmd/organizations/list.go index cc6940f..c011793 100644 --- a/internal/cmd/organizations/list.go +++ b/internal/cmd/organizations/list.go @@ -54,8 +54,7 @@ func listOrgsCommand() *cobra.Command { switch outputFormat { case "yaml": marshaller := &protoyaml.MarshalOptions{ - Indent: 2, - EmitUnpopulated: false, + Indent: 2, } output, err := marshaller.Marshal(listOrgs) if err != nil { @@ -63,11 +62,7 @@ func listOrgsCommand() *cobra.Command { } fmt.Print(string(output)) case "json": - marshaller := &protojson.MarshalOptions{ - Indent: " ", - Multiline: true, - } - output, err := marshaller.Marshal(listOrgs) + output, err := protojson.Marshal(listOrgs) if err != nil { return fmt.Errorf("failed to list organizations: %w", err) } From fde70973f1611fb5ada6a70e11212304d7f79cd7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Sun, 29 Dec 2024 22:40:16 +0000 Subject: [PATCH 4/4] chore: remove unused struct --- internal/cmd/organizations/list.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/internal/cmd/organizations/list.go b/internal/cmd/organizations/list.go index c011793..65df323 100644 --- a/internal/cmd/organizations/list.go +++ b/internal/cmd/organizations/list.go @@ -13,19 +13,6 @@ import ( "go.datum.net/datumctl/internal/resourcemanager" ) -type listResponse struct { - Data struct { - Organizations struct { - Edges []struct { - Node struct { - Name string `json:"name"` - UserEntityID string `json:"userEntityID"` - } `json:"node"` - } `json:"edges"` - } `json:"organizations"` - } `json:"data"` -} - func listOrgsCommand() *cobra.Command { var hostname, outputFormat string