From 1fd8074dedd98dc6579238e847f56ebd25ef7e31 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 11 Sep 2025 16:30:22 +0200 Subject: [PATCH 01/10] feat: add login and token command --- go.mod | 4 +++ go.sum | 10 ++++++ pkg/cmd/cleanupdeployment.go | 5 +-- pkg/cmd/login.go | 70 ++++++++++++++++++++++++++++++++++++ pkg/cmd/root.go | 1 + pkg/renkuapi/auth.go | 52 +++++++++++++++++++++++++++ pkg/renkuapi/constants.go | 3 ++ 7 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 pkg/cmd/login.go create mode 100644 pkg/renkuapi/auth.go create mode 100644 pkg/renkuapi/constants.go diff --git a/go.mod b/go.mod index 7865f64..b61d61a 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.24.2 require ( github.com/spf13/cobra v1.10.1 + github.com/zalando/go-keyring v0.2.6 k8s.io/api v0.34.0 k8s.io/apimachinery v0.34.0 ) require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -16,6 +19,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 05084a6..0c76fc9 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,6 +24,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -29,6 +35,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -86,6 +94,8 @@ 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= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/pkg/cmd/cleanupdeployment.go b/pkg/cmd/cleanupdeployment.go index 557cd29..99539bb 100644 --- a/pkg/cmd/cleanupdeployment.go +++ b/pkg/cmd/cleanupdeployment.go @@ -138,9 +138,10 @@ func askForConfirmation(question string) (response bool, err error) { return false, err } res = strings.ToLower(strings.TrimSpace(res)) - if res == "yes" { + switch res { + case "yes": return true, nil - } else if res == "no" { + case "no": return false, nil } return false, fmt.Errorf("Invalid answer, aborting.") diff --git a/pkg/cmd/login.go b/pkg/cmd/login.go new file mode 100644 index 0000000..6b8a401 --- /dev/null +++ b/pkg/cmd/login.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/github" + ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace" + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/renkuapi" + "github.com/spf13/cobra" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Log in to a renku instance", + Run: login, +} + +func login(cmd *cobra.Command, args []string) { + ctx := context.Background() + + url, err := cmd.Flags().GetString("url") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if url == "" { + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if namespace == "" { + cli, err := github.NewGitHubCLI("") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + namespace, err = ns.FindCurrentNamespace(ctx, cli) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + + deploymentURL, err := ns.GetDeploymentURL(namespace) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + url = deploymentURL.String() + } + + fmt.Printf("URL '%s'\n", url) + + auth, err := renkuapi.NewRenkuApiAuth(url) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + auth.GetAccessToken() +} + +func init() { + loginCmd.Flags().String("url", "", "instance URL") + loginCmd.Flags().StringP("namespace", "n", "", "k8s namespace") +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 5d3e7f8..fe3ae34 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -33,6 +33,7 @@ func runRoot(cmd *cobra.Command, args []string) error { func init() { rootCmd.AddCommand(cleanupDeploymentCmd) rootCmd.AddCommand(copyKeycloakAdminPasswordCmd) + rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(makeMeAdminCmd) rootCmd.AddCommand(namespaceCmd) rootCmd.AddCommand(openDeploymentCmd) diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go new file mode 100644 index 0000000..68d5fea --- /dev/null +++ b/pkg/renkuapi/auth.go @@ -0,0 +1,52 @@ +package renkuapi + +import ( + "fmt" + "net/url" + + "github.com/zalando/go-keyring" +) + +type RenkuApiAuth struct { + baseURL *url.URL + issuerURL *url.URL +} + +func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) { + parsedURL, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + if parsedURL.EscapedPath() == "/" { + parsedURL.Path = "" + } + auth = &RenkuApiAuth{ + baseURL: parsedURL, + } + if auth.issuerURL == nil { + auth.issuerURL = parsedURL.JoinPath("auth/realms/Renku") + } + return auth, nil +} + +func (auth *RenkuApiAuth) GetAccessToken() (token string, err error) { + token, err = auth.getAccessTokenFromKeyring() + fmt.Println(token) + fmt.Println(err) + + return "", fmt.Errorf("not implemented") +} + +func (auth *RenkuApiAuth) getAccessTokenFromKeyring() (token string, err error) { + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token") + return keyring.Get(keyringService, kUser) +} + +func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) { + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token") + return keyring.Get(keyringService, kUser) +} + +func (auth *RenkuApiAuth) getKeyringUserPrefix() string { + return fmt.Sprintf("rdu:%s", auth.baseURL.String()) +} diff --git a/pkg/renkuapi/constants.go b/pkg/renkuapi/constants.go new file mode 100644 index 0000000..0b009de --- /dev/null +++ b/pkg/renkuapi/constants.go @@ -0,0 +1,3 @@ +package renkuapi + +const keyringService string = "renku-dev-utils" From 4191661e149e7f80b3c3b017c046e4db631d72b1 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 11 Sep 2025 16:36:40 +0200 Subject: [PATCH 02/10] update const --- pkg/renkuapi/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/renkuapi/constants.go b/pkg/renkuapi/constants.go index 0b009de..9025255 100644 --- a/pkg/renkuapi/constants.go +++ b/pkg/renkuapi/constants.go @@ -1,3 +1,3 @@ package renkuapi -const keyringService string = "renku-dev-utils" +const keyringService string = "github.com/SwissDataScienceCenter/renku-dev-utils" From 5f9a96c67de21109a99aa89e21961544ea02db4e Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 12 Sep 2025 09:16:21 +0200 Subject: [PATCH 03/10] wip --- pkg/cmd/login.go | 6 +- pkg/keycloak/rest.go | 1 - pkg/renkuapi/auth.go | 208 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 211 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/login.go b/pkg/cmd/login.go index 6b8a401..f0f3935 100644 --- a/pkg/cmd/login.go +++ b/pkg/cmd/login.go @@ -61,7 +61,11 @@ func login(cmd *cobra.Command, args []string) { os.Exit(1) } - auth.GetAccessToken() + err = auth.Login(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } func init() { diff --git a/pkg/keycloak/rest.go b/pkg/keycloak/rest.go index 2e0ca34..da03e08 100644 --- a/pkg/keycloak/rest.go +++ b/pkg/keycloak/rest.go @@ -102,7 +102,6 @@ func (client *KeycloakClient) PostForm(ctx context.Context, url string, data url } return resp, nil - } func tryParseResponse(resp *http.Response, result any) error { diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go index 68d5fea..7332e44 100644 --- a/pkg/renkuapi/auth.go +++ b/pkg/renkuapi/auth.go @@ -1,15 +1,30 @@ package renkuapi import ( + "bytes" + "context" + "encoding/json" "fmt" + "net/http" "net/url" + "strings" + "time" "github.com/zalando/go-keyring" ) +const jsonContentType string = "application/json" + type RenkuApiAuth struct { - baseURL *url.URL - issuerURL *url.URL + baseURL *url.URL + issuerURL *url.URL + authenticationURI *url.URL + tokenURI *url.URL + + clientID string + scope string + + httpClient *http.Client } func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) { @@ -26,6 +41,15 @@ func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) { if auth.issuerURL == nil { auth.issuerURL = parsedURL.JoinPath("auth/realms/Renku") } + if auth.clientID == "" { + auth.clientID = "renku-cli" + } + if auth.scope == "" { + auth.scope = "offline_access" + } + if auth.httpClient == nil { + auth.httpClient = http.DefaultClient + } return auth, nil } @@ -50,3 +74,183 @@ func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) func (auth *RenkuApiAuth) getKeyringUserPrefix() string { return fmt.Sprintf("rdu:%s", auth.baseURL.String()) } + +func (auth *RenkuApiAuth) Login(ctx context.Context) error { + err := auth.performLogin(ctx) + if err != nil { + return err + } + return nil +} + +func (auth *RenkuApiAuth) performLogin(ctx context.Context) error { + deviceAuthorization, err := auth.startLogin(ctx) + if err != nil { + return err + } + fmt.Printf("deviceAuthorization: %+v\n", deviceAuthorization) + return fmt.Errorf("not implemented") +} + +func (auth *RenkuApiAuth) startLogin(ctx context.Context) (result deviceAuthorization, err error) { + authenticationURI, err := auth.getAuthenticationURI(ctx) + if err != nil { + return result, err + } + + body := url.Values{} + body.Set("client_id", auth.clientID) + body.Set("scope", auth.scope) + + var res deviceAuthorizationResponse + _, err = auth.postForm(ctx, authenticationURI.String(), body, &res) + if err != nil { + return result, err + } + + result = deviceAuthorization{ + DeviceCode: res.DeviceCode, + VerificationURIComplete: res.VerificationURIComplete, + ExpiresAt: time.Now().Add(time.Second * time.Duration(res.ExpiresIn)), + Interval: time.Second * time.Duration(res.Interval), + } + if result.Interval == time.Duration(0) { + result.Interval = time.Second * 5 + } + return result, nil +} + +type deviceAuthorization struct { + DeviceCode string + VerificationURIComplete string + ExpiresAt time.Time + Interval time.Duration +} + +type deviceAuthorizationResponse struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete"` + ExpiresIn int32 `json:"expires_in"` + Interval int32 `json:"interval"` +} + +func (auth *RenkuApiAuth) getAuthenticationURI(ctx context.Context) (authenticationURI *url.URL, err error) { + if auth.authenticationURI != nil { + return auth.authenticationURI, nil + } + err = auth.getOpenIDConfiguration(ctx) + if err != nil { + return nil, err + } + return auth.authenticationURI, nil +} + +func (auth *RenkuApiAuth) getTokenURI(ctx context.Context) (tokenURI *url.URL, err error) { + if auth.tokenURI != nil { + return auth.tokenURI, nil + } + err = auth.getOpenIDConfiguration(ctx) + if err != nil { + return nil, err + } + return auth.tokenURI, nil +} + +func (auth *RenkuApiAuth) getOpenIDConfiguration(ctx context.Context) error { + configurationURL := auth.issuerURL.JoinPath("./.well-known/openid-configuration") + fmt.Printf("configurationURL: %s\n", configurationURL.String()) + var result openIDConfigurationResponse + _, err := auth.get(ctx, configurationURL.String(), &result) + if err != nil { + return err + } + + fmt.Printf("result: %+v\n", result) + + parsed, err := url.Parse(result.DeviceAuthorizationEndpoint) + if err != nil { + return err + } + auth.authenticationURI = parsed + + parsed, err = url.Parse(result.TokenEndpoint) + if err != nil { + return err + } + auth.tokenURI = parsed + return nil +} + +type openIDConfigurationResponse struct { + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` +} + +// TODO: refactor this method to avoid duplication with package keycloak + +func (auth *RenkuApiAuth) get(ctx context.Context, url string, result any) (resp *http.Response, err error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", jsonContentType) + + resp, err = auth.httpClient.Do(req) + if err != nil { + return resp, err + } + + var parseErr error + if resp.Header.Get("Content-Type") == jsonContentType { + parseErr = tryParseResponse(resp, result) + } else { + return resp, fmt.Errorf("Expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) + } + if resp.StatusCode >= 200 && resp.StatusCode < 300 && parseErr != nil { + return resp, parseErr + } + + return resp, nil +} + +func (auth *RenkuApiAuth) postForm(ctx context.Context, url string, data url.Values, result any) (resp *http.Response, err error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", jsonContentType) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err = auth.httpClient.Do(req) + if err != nil { + return resp, err + } + + var parseErr error + if resp.Header.Get("Content-Type") == jsonContentType { + parseErr = tryParseResponse(resp, result) + } else { + return resp, fmt.Errorf("Expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) + } + if resp.StatusCode >= 200 && resp.StatusCode < 300 && parseErr != nil { + return resp, parseErr + } + + return resp, nil +} + +func tryParseResponse(resp *http.Response, result any) error { + defer resp.Body.Close() + + outBuf := new(bytes.Buffer) + _, err := outBuf.ReadFrom(resp.Body) + if err != nil { + return err + } + + return json.Unmarshal(outBuf.Bytes(), result) +} From 49dcf8a03dc3e5b891decff8620df2b81fca432e Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 12 Sep 2025 09:48:50 +0200 Subject: [PATCH 04/10] wip --- pkg/renkuapi/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go index 7332e44..a342a59 100644 --- a/pkg/renkuapi/auth.go +++ b/pkg/renkuapi/auth.go @@ -207,7 +207,7 @@ func (auth *RenkuApiAuth) get(ctx context.Context, url string, result any) (resp if resp.Header.Get("Content-Type") == jsonContentType { parseErr = tryParseResponse(resp, result) } else { - return resp, fmt.Errorf("Expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) + return resp, fmt.Errorf("expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) } if resp.StatusCode >= 200 && resp.StatusCode < 300 && parseErr != nil { return resp, parseErr @@ -234,7 +234,7 @@ func (auth *RenkuApiAuth) postForm(ctx context.Context, url string, data url.Val if resp.Header.Get("Content-Type") == jsonContentType { parseErr = tryParseResponse(resp, result) } else { - return resp, fmt.Errorf("Expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) + return resp, fmt.Errorf("expected '%s' but got response with content type '%s'", jsonContentType, resp.Header.Get("Content-Type")) } if resp.StatusCode >= 200 && resp.StatusCode < 300 && parseErr != nil { return resp, parseErr From 9032055593f6a6cae69d0b769816e8dcc19668a5 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Mon, 15 Sep 2025 10:44:03 +0200 Subject: [PATCH 05/10] wip: login --- go.mod | 1 + go.sum | 2 + pkg/renkuapi/auth.go | 223 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 216 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index b61d61a..2148a08 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/SwissDataScienceCenter/renku-dev-utils go 1.24.2 require ( + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/spf13/cobra v1.10.1 github.com/zalando/go-keyring v0.2.6 k8s.io/api v0.34.0 diff --git a/go.sum b/go.sum index 0c76fc9..3bade72 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go index a342a59..05cdbc4 100644 --- a/pkg/renkuapi/auth.go +++ b/pkg/renkuapi/auth.go @@ -7,9 +7,13 @@ import ( "fmt" "net/http" "net/url" + "os/exec" + "runtime" "strings" "time" + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/executils" + "github.com/golang-jwt/jwt/v5" "github.com/zalando/go-keyring" ) @@ -24,6 +28,9 @@ type RenkuApiAuth struct { clientID string scope string + accessToken string + refreshToken string + httpClient *http.Client } @@ -53,17 +60,80 @@ func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) { return auth, nil } -func (auth *RenkuApiAuth) GetAccessToken() (token string, err error) { +func (auth *RenkuApiAuth) GetAccessToken(ctx context.Context) (token string, err error) { + // Use access token if valid + token = auth.accessToken + if token != "" && isTokenValid(token) { + return token, nil + } token, err = auth.getAccessTokenFromKeyring() - fmt.Println(token) - fmt.Println(err) + if err != nil { + token = "" + } + if token != "" && isTokenValid(token) { + return token, nil + } - return "", fmt.Errorf("not implemented") + // Refresh the access token if possible + refreshToken := auth.refreshToken + if refreshToken == "" { + refreshToken, err = auth.getRefreshTokenFromKeyring() + if err != nil { + refreshToken = "" + } + } + if refreshToken == "" { + return "", fmt.Errorf("could not get access token") + } + tokenResult, err := auth.postRefeshToken(ctx, refreshToken) + if err != nil { + return token, nil + } + auth.accessToken = tokenResult.AccessToken + auth.refreshToken = tokenResult.RefreshToken + err = auth.saveAccessTokenToKeyring() + if err != nil { + return auth.accessToken, err + } + err = auth.saveRefreshTokenToKeyring() + if err != nil { + return auth.accessToken, err + } + return auth.accessToken, nil +} + +func isTokenValid(token string) (isValid bool) { + claims := jwt.RegisteredClaims{} + parser := jwt.NewParser() + _, _, err := parser.ParseUnverified(token, &claims) + if err != nil { + return false + } + exp, err := claims.GetExpirationTime() + if err != nil || exp == nil { + return false + } + now := time.Now() + leeway := time.Second * 10 + return now.Before(exp.Add(-leeway)) } func (auth *RenkuApiAuth) getAccessTokenFromKeyring() (token string, err error) { kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token") - return keyring.Get(keyringService, kUser) + token, err = keyring.Get(keyringService, kUser) + if err != nil { + return token, err + } + auth.accessToken = token + return token, nil +} + +func (auth *RenkuApiAuth) saveAccessTokenToKeyring() (err error) { + if auth.accessToken == "" { + return fmt.Errorf("access_token is not set") + } + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token") + return keyring.Set(keyringService, kUser, auth.accessToken) } func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) { @@ -71,11 +141,24 @@ func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) return keyring.Get(keyringService, kUser) } +func (auth *RenkuApiAuth) saveRefreshTokenToKeyring() (err error) { + if auth.refreshToken == "" { + return fmt.Errorf("refresh_token is not set") + } + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token") + return keyring.Set(keyringService, kUser, auth.refreshToken) +} + func (auth *RenkuApiAuth) getKeyringUserPrefix() string { return fmt.Sprintf("rdu:%s", auth.baseURL.String()) } func (auth *RenkuApiAuth) Login(ctx context.Context) error { + // TODO: check username from API + token, _ := auth.GetAccessToken(ctx) + if token != "" { + return nil + } err := auth.performLogin(ctx) if err != nil { return err @@ -88,8 +171,21 @@ func (auth *RenkuApiAuth) performLogin(ctx context.Context) error { if err != nil { return err } - fmt.Printf("deviceAuthorization: %+v\n", deviceAuthorization) - return fmt.Errorf("not implemented") + err = openBrowser(ctx, deviceAuthorization.VerificationURIComplete) + if err != nil { + return err + } + tokenResult, err := auth.pollTokenEndpoint(ctx, deviceAuthorization) + if err != nil { + return err + } + auth.accessToken = tokenResult.AccessToken + auth.refreshToken = tokenResult.RefreshToken + err = auth.saveAccessTokenToKeyring() + if err != nil { + return err + } + return auth.saveRefreshTokenToKeyring() } func (auth *RenkuApiAuth) startLogin(ctx context.Context) (result deviceAuthorization, err error) { @@ -160,15 +256,12 @@ func (auth *RenkuApiAuth) getTokenURI(ctx context.Context) (tokenURI *url.URL, e func (auth *RenkuApiAuth) getOpenIDConfiguration(ctx context.Context) error { configurationURL := auth.issuerURL.JoinPath("./.well-known/openid-configuration") - fmt.Printf("configurationURL: %s\n", configurationURL.String()) var result openIDConfigurationResponse _, err := auth.get(ctx, configurationURL.String(), &result) if err != nil { return err } - fmt.Printf("result: %+v\n", result) - parsed, err := url.Parse(result.DeviceAuthorizationEndpoint) if err != nil { return err @@ -254,3 +347,113 @@ func tryParseResponse(resp *http.Response, result any) error { return json.Unmarshal(outBuf.Bytes(), result) } + +// TODO: refactor this to avoid duplication with opendeployment.go +func openBrowser(ctx context.Context, openURL string) error { + if runtime.GOOS == "darwin" { + fmt.Printf("Opening: %s\n", openURL) + cmd := exec.CommandContext(ctx, "open", openURL) + _, err := executils.FormatOutput(cmd.Output()) + if err != nil { + return err + } + return nil + } + + if runtime.GOOS == "linux" { + fmt.Printf("Opening: %s\n", openURL) + cmd := exec.CommandContext(ctx, "xdg-open", openURL) + _, err := executils.FormatOutput(cmd.Output()) + if err != nil { + return err + } + return nil + } + + fmt.Printf("Open this link in your browser: %s\n", openURL) + return nil +} + +func (auth *RenkuApiAuth) pollTokenEndpoint(ctx context.Context, deviceAuthorization deviceAuthorization) (result tokenResult, err error) { + deadline, cancel := context.WithDeadline(ctx, deviceAuthorization.ExpiresAt.Add(deviceAuthorization.Interval)) + defer cancel() + + ticker := time.NewTicker(deviceAuthorization.Interval) + defer ticker.Stop() + + for { + select { + case <-deadline.Done(): + return result, deadline.Err() + case <-ticker.C: + result, err := auth.postToken(deadline, deviceAuthorization.DeviceCode) + if err == nil { + return result, nil + } + } + } +} + +func (auth *RenkuApiAuth) postToken(ctx context.Context, deviceCode string) (result tokenResult, err error) { + tokenURI, err := auth.getTokenURI(ctx) + if err != nil { + return result, err + } + + body := url.Values{} + body.Set("client_id", auth.clientID) + body.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + body.Set("device_code", deviceCode) + + var res tokenResponse + _, err = auth.postForm(ctx, tokenURI.String(), body, &res) + if err != nil { + return result, err + } + + result = tokenResult{ + AccessToken: res.AccessToken, + RefreshToken: res.RefreshToken, + } + return result, nil +} + +type tokenResult struct { + AccessToken string + RefreshToken string +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int32 `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + RefreshExpiresIn int32 `json:"refresh_expires_in"` + TokenType string `json:"token_type"` + NotBeforePolicy int32 `json:"not-before-policy"` + SessionState string `json:"session_state"` + Scope string `json:"scope"` +} + +func (auth *RenkuApiAuth) postRefeshToken(ctx context.Context, refreshToken string) (result tokenResult, err error) { + tokenURI, err := auth.getTokenURI(ctx) + if err != nil { + return result, err + } + + body := url.Values{} + body.Set("client_id", auth.clientID) + body.Set("grant_type", "refresh_token") + body.Set("refresh_token", refreshToken) + + var res tokenResponse + _, err = auth.postForm(ctx, tokenURI.String(), body, &res) + if err != nil { + return result, err + } + + result = tokenResult{ + AccessToken: res.AccessToken, + RefreshToken: res.RefreshToken, + } + return result, nil +} From e763875a6e63addb66866706a0caca89d4455ca0 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Wed, 17 Sep 2025 08:50:54 +0200 Subject: [PATCH 06/10] wip: can show login info --- Makefile | 14 + go.mod | 20 +- go.sum | 128 +- pkg/cmd/login.go | 23 +- pkg/renkuapi/auth.go | 20 +- pkg/renkuapi/users/api.spec.yaml | 611 +++++++ pkg/renkuapi/users/users.go | 84 + pkg/renkuapi/users/users_gen.go | 2567 ++++++++++++++++++++++++++++++ pkg/tools/tools.go | 5 + 9 files changed, 3464 insertions(+), 8 deletions(-) create mode 100644 pkg/renkuapi/users/api.spec.yaml create mode 100644 pkg/renkuapi/users/users.go create mode 100644 pkg/renkuapi/users/users_gen.go create mode 100644 pkg/tools/tools.go diff --git a/Makefile b/Makefile index 5b95f7b..8e3246a 100644 --- a/Makefile +++ b/Makefile @@ -68,3 +68,17 @@ check-format: ## Check that sources are correctly formatted .PHONY: check-vet check-vet: ## Check source files with `go vet` go vet ./... + +##@ Code generation + +.PHONY: renku-users-apispec +renku-users-apispec: ## Download the "users" API spec + curl -L -o pkg/renkuapi/users/api.spec.yaml https://raw.githubusercontent.com/SwissDataScienceCenter/renku-data-services/refs/heads/main/components/renku_data_services/users/api.spec.yaml + sed -e 's/- default: "general"//g' pkg/renkuapi/users/api.spec.yaml > pkg/renkuapi/users/api.spec.new.yaml + mv pkg/renkuapi/users/api.spec.new.yaml pkg/renkuapi/users/api.spec.yaml + +.PHONY: generate +generate: pkg/renkuapi/users/users_gen.go ## Run go generate + +pkg/renkuapi/users/users_gen.go: pkg/renkuapi/users/api.spec.yaml + go generate pkg/renkuapi/users/users.go diff --git a/go.mod b/go.mod index 2148a08..26acbdd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/SwissDataScienceCenter/renku-dev-utils go 1.24.2 require ( + github.com/getkin/kin-openapi v0.132.0 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 + github.com/oapi-codegen/runtime v1.1.2 github.com/spf13/cobra v1.10.1 github.com/zalando/go-keyring v0.2.6 k8s.io/api v0.34.0 @@ -12,8 +15,10 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -29,23 +34,34 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/image v0.28.0 // indirect golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.30.0 // indirect + golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect diff --git a/go.sum b/go.sum index 3bade72..4d87da6 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,13 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +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= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= @@ -8,10 +15,18 @@ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -22,33 +37,55 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -64,12 +101,38 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 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= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -77,21 +140,34 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= 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= @@ -119,27 +195,51 @@ golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mT golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= @@ -148,6 +248,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= @@ -155,15 +256,34 @@ 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 v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= diff --git a/pkg/cmd/login.go b/pkg/cmd/login.go index f0f3935..0bcdbf8 100644 --- a/pkg/cmd/login.go +++ b/pkg/cmd/login.go @@ -8,6 +8,7 @@ import ( "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/github" ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace" "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/renkuapi" + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/renkuapi/users" "github.com/spf13/cobra" ) @@ -53,7 +54,7 @@ func login(cmd *cobra.Command, args []string) { url = deploymentURL.String() } - fmt.Printf("URL '%s'\n", url) + fmt.Printf("Renku URL: %s\n", url) auth, err := renkuapi.NewRenkuApiAuth(url) if err != nil { @@ -66,6 +67,26 @@ func login(cmd *cobra.Command, args []string) { fmt.Println(err) os.Exit(1) } + + ruc, err := users.NewRenkuUsersClient(url, users.WithRequestEditors(users.RequestEditorFn(auth.RequestEditor()))) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + userInfo, err := ruc.GetUser(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Println("Logged in as:") + fmt.Printf(" username: %s\n", userInfo.Username) + fmt.Printf(" email: %s\n", *userInfo.Email) + fmt.Printf(" first name: %s\n", *userInfo.FirstName) + fmt.Printf(" last name: %s\n", *userInfo.LastName) + fmt.Printf(" is admin: %t\n", userInfo.IsAdmin) + } func init() { diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go index 05cdbc4..d920fb1 100644 --- a/pkg/renkuapi/auth.go +++ b/pkg/renkuapi/auth.go @@ -60,6 +60,25 @@ func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) { return auth, nil } +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// RequestEditor returns a request editor which injects a valid access token +// for API requests. +func (auth *RenkuApiAuth) RequestEditor() RequestEditorFn { + return func(ctx context.Context, req *http.Request) error { + if req.Header.Get("Authorization") != "" { + return nil + } + token, err := auth.GetAccessToken(ctx) + if err != nil { + return err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + return nil + } +} + func (auth *RenkuApiAuth) GetAccessToken(ctx context.Context) (token string, err error) { // Use access token if valid token = auth.accessToken @@ -154,7 +173,6 @@ func (auth *RenkuApiAuth) getKeyringUserPrefix() string { } func (auth *RenkuApiAuth) Login(ctx context.Context) error { - // TODO: check username from API token, _ := auth.GetAccessToken(ctx) if token != "" { return nil diff --git a/pkg/renkuapi/users/api.spec.yaml b/pkg/renkuapi/users/api.spec.yaml new file mode 100644 index 0000000..2e283a4 --- /dev/null +++ b/pkg/renkuapi/users/api.spec.yaml @@ -0,0 +1,611 @@ +--- +openapi: 3.0.2 +info: + title: Renku Data Services API + description: | + This service is the main backend for Renku. It provides information about users, projects, + cloud storage, access to compute resources and many other things. + version: v1 +servers: + - url: /api/data +paths: + /user: + get: + summary: Get information about the currently logged in user + responses: + "200": + description: The currently logged in user + content: + "application/json": + schema: + $ref: "#/components/schemas/SelfUserInfo" + default: + $ref: "#/components/responses/Error" + tags: + - users + /user/secret_key: + get: + summary: Get the current users secret key + responses: + "200": + description: | + The secret key of the currently logged in user. + This endpoint is not publicly accessible. + content: + "application/json": + schema: + $ref: "#/components/schemas/UserSecretKey" + default: + $ref: "#/components/responses/Error" + tags: + - secrets + /users: + get: + summary: List all users + parameters: + - in: query + style: form + explode: true + name: user_params + schema: + type: object + additionalProperties: false + properties: + exact_email: + type: string + description: Return the user(s) with an exact match on the email provided + responses: + "200": + description: The list of users in the service (this is a subset of what is in Keycloak) + content: + "application/json": + schema: + $ref: "#/components/schemas/UsersWithId" + default: + $ref: "#/components/responses/Error" + tags: + - users + /users/{user_id}: + get: + summary: Get a specific user by their Keycloak ID + parameters: + - in: path + name: user_id + required: true + schema: + type: string + responses: + "200": + description: The requested user + content: + "application/json": + schema: + $ref: "#/components/schemas/UserWithId" + "404": + description: The user does not exist + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + default: + $ref: "#/components/responses/Error" + tags: + - users + delete: + summary: Delete a specific user by their Keycloak ID + parameters: + - in: path + name: user_id + required: true + schema: + type: string + responses: + "204": + description: The user was successfully deleted or did not exist + default: + $ref: "#/components/responses/Error" + tags: + - users + /user/secrets: + get: + summary: List all secrets for a user (keys only) + parameters: + - in: query + style: form + explode: true + name: user_secrets_params + schema: + type: object + additionalProperties: false + properties: + kind: + description: Filter results based on secret kind + $ref: "#/components/schemas/SecretKind" + default: general + responses: + "200": + description: The user's secrets + content: + "application/json": + schema: + $ref: "#/components/schemas/SecretsList" + "404": + description: The user does not exist + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + default: + $ref: "#/components/responses/Error" + tags: + - secrets + post: + summary: Create a new secret for the user + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SecretPost" + responses: + "201": + description: Secret successfully created + content: + "application/json": + schema: + $ref: "#/components/schemas/SecretWithId" + default: + $ref: "#/components/responses/Error" + tags: + - secrets + /user/secrets/{secret_id}: + get: + summary: Get a secret key by id + parameters: + - in: path + name: secret_id + required: true + schema: + $ref: "#/components/schemas/Ulid" + responses: + "200": + description: The secret + content: + "application/json": + schema: + $ref: "#/components/schemas/SecretWithId" + "404": + description: The user does not exist + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + default: + $ref: "#/components/responses/Error" + tags: + - secrets + patch: + summary: Change a secret + parameters: + - in: path + name: secret_id + required: true + schema: + $ref: "#/components/schemas/Ulid" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SecretPatch" + responses: + "201": + description: Secret successfully changed + content: + "application/json": + schema: + $ref: "#/components/schemas/SecretWithId" + default: + $ref: "#/components/responses/Error" + tags: + - secrets + delete: + summary: Delete a secret + parameters: + - in: path + name: secret_id + required: true + schema: + $ref: "#/components/schemas/Ulid" + responses: + "204": + description: The secret was deleted or didn't exist + default: + $ref: "#/components/responses/Error" + tags: + - secrets + + /error: + get: + summary: Get a sample error response with status code 422 + responses: + "422": + description: The error + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + /version: + get: + summary: Get the version of the service + responses: + "200": + description: The error + content: + "application/json": + schema: + $ref: "#/components/schemas/Version" + + "/user/preferences": + get: + summary: Get user preferences for the currently logged in user + responses: + "200": + description: The user preferences + content: + "application/json": + schema: + $ref: "#/components/schemas/UserPreferences" + "404": + description: The user has no preferences saved + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + default: + $ref: "#/components/responses/Error" + tags: + - user_preferences + "/user/preferences/dismiss_project_migration_banner": + post: + summary: Dismiss banner project migration + responses: + "200": + description: The updated user preferences + content: + "application/json": + schema: + $ref: "#/components/schemas/UserPreferences" + default: + $ref: "#/components/responses/Error" + tags: + - user_preferences + delete: + summary: Remove dismiss banner project migration + responses: + "200": + description: The updated user preferences + content: + "application/json": + schema: + $ref: "#/components/schemas/UserPreferences" + default: + $ref: "#/components/responses/Error" + tags: + - user_preferences + "/user/preferences/pinned_projects": + post: + summary: Add a pinned project + requestBody: + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/AddPinnedProject" + responses: + "200": + description: The updated user preferences + content: + "application/json": + schema: + $ref: "#/components/schemas/UserPreferences" + default: + $ref: "#/components/responses/Error" + tags: + - user_preferences + delete: + summary: Remove one or all pinned projects + parameters: + - in: query + description: query parameters + name: delete_pinned_params + style: form + explode: true + schema: + type: object + additionalProperties: false + properties: + project_slug: + type: string + default: "" + responses: + "200": + description: The updated user preferences + content: + "application/json": + schema: + $ref: "#/components/schemas/UserPreferences" + default: + $ref: "#/components/responses/Error" + tags: + - user_preferences + +components: + schemas: + UserWithId: + type: object + additionalProperties: false + properties: + id: + $ref: "#/components/schemas/UserId" + username: + $ref: "#/components/schemas/Username" + email: + $ref: "#/components/schemas/UserEmail" + first_name: + $ref: "#/components/schemas/UserFirstLastName" + last_name: + $ref: "#/components/schemas/UserFirstLastName" + required: + - id + - username + example: + id: "some-random-keycloak-id" + username: "some-username" + first_name: "Jane" + last_name: "Doe" + UsersWithId: + type: array + items: + $ref: "#/components/schemas/UserWithId" + uniqueItems: true + SelfUserInfo: + description: Information about the currently logged in user + type: object + additionalProperties: false + properties: + id: + $ref: "#/components/schemas/UserId" + username: + $ref: "#/components/schemas/Username" + email: + $ref: "#/components/schemas/UserEmail" + first_name: + $ref: "#/components/schemas/UserFirstLastName" + last_name: + $ref: "#/components/schemas/UserFirstLastName" + is_admin: + description: Whether the user is a platform administrator or not + type: boolean + default: false + required: + - id + - username + - is_admin + UserSecretKey: + type: object + additionalProperties: false + properties: + secret_key: + type: string + description: The users secret key + Version: + type: object + properties: + version: + type: string + required: ["version"] + UserId: + type: string + description: Keycloak user ID + example: f74a228b-1790-4276-af5f-25c2424e9b0c + pattern: "^[A-Za-z0-9]{1}[A-Za-z0-9-]+$" + Username: + type: string + description: Handle of the user + example: some-username + minLength: 1 + maxLength: 99 + UserFirstLastName: + type: string + description: First or last name of the user + example: John + minLength: 1 + maxLength: 256 + UserEmail: + type: string + # We can get invalid emails when people log in via Github or other services + # It seems that in some cases Keycloak does not verify emails and this will then fail + # See: https://github.com/SwissDataScienceCenter/renku-data-services/issues/367 + # format: email + description: User email + example: some-user@gmail.com + SecretsList: + description: A list of secrets + type: array + items: + $ref: "#/components/schemas/SecretWithId" + minItems: 0 + SecretWithId: + description: A Renku secret + type: object + additionalProperties: false + properties: + id: + $ref: "#/components/schemas/Ulid" + name: + $ref: "#/components/schemas/SecretName" + default_filename: + $ref: "#/components/schemas/SecretDefaultFilename" + modification_date: + $ref: "#/components/schemas/ModificationDate" + kind: + $ref: "#/components/schemas/SecretKind" + session_secret_slot_ids: + $ref: "#/components/schemas/UlidList" + data_connector_ids: + $ref: "#/components/schemas/UlidList" + required: + - "id" + - "name" + - "default_filename" + - "modification_date" + - "kind" + - "session_secret_slot_ids" + - "data_connector_ids" + SecretPost: + description: Secret metadata to be created + type: object + additionalProperties: false + properties: + name: + $ref: "#/components/schemas/SecretName" + default_filename: + $ref: "#/components/schemas/SecretDefaultFilename" + value: + $ref: "#/components/schemas/SecretValue" + kind: + allOf: + - $ref: "#/components/schemas/SecretKind" + + default: general + required: + - "name" + - "value" + SecretPatch: + type: object + description: Secret metadata to be modified + additionalProperties: false + properties: + name: + $ref: "#/components/schemas/SecretName" + default_filename: + $ref: "#/components/schemas/SecretDefaultFilename" + value: + $ref: "#/components/schemas/SecretValue" + SecretName: + description: The name of a user secret + type: string + minLength: 1 + maxLength: 99 + example: API Token + SecretDefaultFilename: + description: | + Filename to give to this secret when mounted in Renku 1.0 sessions + type: string + minLength: 1 + maxLength: 99 + pattern: "^[a-zA-Z0-9_\\-.]*$" + example: "Data-S3-Secret_1" + Ulid: + description: ULID identifier + type: string + minLength: 26 + maxLength: 26 + pattern: "^[0-7][0-9A-HJKMNP-TV-Z]{25}$" # This is case-insensitive + UlidList: + type: array + items: + $ref: "#/components/schemas/Ulid" + ModificationDate: + description: The date and time the secret was created or modified (this is always in UTC) + type: string + format: date-time + example: "2023-11-01T17:32:28Z" + SecretValue: + description: Secret value that can be any text + type: string + minLength: 1 + maxLength: 5000 + SecretKind: + description: Kind of secret + type: string + enum: + - general + - storage + ShowProjectMigrationBanner: + description: Should display project migration banner + type: boolean + UserPreferences: + type: object + description: The object containing user preferences + additionalProperties: false + properties: + user_id: + $ref: "#/components/schemas/UserId" + pinned_projects: + $ref: "#/components/schemas/PinnedProjects" + show_project_migration_banner: + $ref: "#/components/schemas/ShowProjectMigrationBanner" + required: ["user_id", "pinned_projects"] + PinnedProjects: + type: object + description: The list of projects a user has pinned on their dashboard + properties: + project_slugs: + type: array + items: + $ref: "#/components/schemas/ProjectSlugResponse" + ProjectSlug: + type: string + description: The slug used to identify a project + minLength: 3 + example: "user/my-project" + # limitations based on allowed characters in project slugs from Gitlab from here: + # https://docs.gitlab.com/ee/user/reserved_names.html + pattern: "^[a-zA-Z0-9]+([_.\\-/][a-zA-Z0-9]+)*[_.\\-/]?[a-zA-Z0-9]$" + ProjectSlugResponse: + type: string + description: The slug used to identify a project + minLength: 3 + example: "user/my-project" + AddPinnedProject: + type: object + additionalProperties: false + properties: + project_slug: + $ref: "#/components/schemas/ProjectSlug" + required: ["project_slug"] + ErrorResponse: + type: object + properties: + error: + type: object + properties: + code: + type: integer + minimum: 0 + exclusiveMinimum: true + example: 1404 + detail: + type: string + example: "A more detailed optional message showing what the problem was" + message: + type: string + example: "Something went wrong - please try again later" + required: ["code", "message"] + required: ["error"] + responses: + Error: + description: The schema for all 4xx and 5xx responses + content: + "application/json": + schema: + $ref: "#/components/schemas/ErrorResponse" + securitySchemes: + oidc: + type: openIdConnect + openIdConnectUrl: /auth/realms/Renku/.well-known/openid-configuration +security: + - oidc: + - openid diff --git a/pkg/renkuapi/users/users.go b/pkg/renkuapi/users/users.go new file mode 100644 index 0000000..1bd4674 --- /dev/null +++ b/pkg/renkuapi/users/users.go @@ -0,0 +1,84 @@ +package users + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" +) + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -generate types,client,spec -package users -o users_gen.go api.spec.yaml + +type RenkuUsersClient struct { + bacseClient *ClientWithResponses + httpClient *http.Client + requestEditors []RequestEditorFn +} + +func NewRenkuUsersClient(apiURL string, options ...RenkuUsersClientOption) (c *RenkuUsersClient, err error) { + c = &RenkuUsersClient{} + for _, opt := range options { + if err := opt(c); err != nil { + return nil, err + } + } + parsedURL, err := url.Parse(apiURL) + if err != nil { + return nil, err + } + // Ensure we use the correct API URL + if !strings.HasSuffix(parsedURL.EscapedPath(), "/api/data") { + parsedURL.Path = strings.TrimSuffix(parsedURL.Path, "/") + "/" + parsedURL = parsedURL.JoinPath("api/data") + } + // Create httpClient, if not already present + if c.httpClient == nil { + c.httpClient = http.DefaultClient + } + // Create client + clientOpts := []ClientOption{WithHTTPClient(c.httpClient)} + for _, fn := range c.requestEditors { + clientOpts = append(clientOpts, WithRequestEditorFn(fn)) + } + client, err := NewClientWithResponses(parsedURL.String(), clientOpts...) + if err != nil { + return nil, err + } + c.bacseClient = client + return c, nil +} + +type RenkuUsersClientOption func(*RenkuUsersClient) error + +func WithHttpClient(httpClient *http.Client) RenkuUsersClientOption { + return func(c *RenkuUsersClient) error { + c.httpClient = httpClient + return nil + } +} + +func WithRequestEditors(editors ...RequestEditorFn) RenkuUsersClientOption { + return func(c *RenkuUsersClient) error { + c.requestEditors = append(c.requestEditors, editors...) + return nil + } +} + +func (c *RenkuUsersClient) GetUser(ctx context.Context) (userInfo SelfUserInfo, err error) { + res, err := c.bacseClient.GetUserWithResponse(ctx) + if err != nil { + return userInfo, err + } + if res.JSON200 == nil { + message := "" + if res.JSONDefault != nil { + message = res.JSONDefault.Error.Message + } + if message != "" { + return userInfo, fmt.Errorf("could not get user info: %s", message) + } + return userInfo, fmt.Errorf("could not get user info: HTTP %d", res.StatusCode()) + } + return *res.JSON200, nil +} diff --git a/pkg/renkuapi/users/users_gen.go b/pkg/renkuapi/users/users_gen.go new file mode 100644 index 0000000..8a0be7f --- /dev/null +++ b/pkg/renkuapi/users/users_gen.go @@ -0,0 +1,2567 @@ +// Package users provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. +package users + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/oapi-codegen/runtime" +) + +const ( + OidcScopes = "oidc.Scopes" +) + +// Defines values for SecretKind. +const ( + General SecretKind = "general" + Storage SecretKind = "storage" +) + +// AddPinnedProject defines model for AddPinnedProject. +type AddPinnedProject struct { + // ProjectSlug The slug used to identify a project + ProjectSlug ProjectSlug `json:"project_slug"` +} + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + Error struct { + Code int `json:"code"` + Detail *string `json:"detail,omitempty"` + Message string `json:"message"` + } `json:"error"` +} + +// ModificationDate The date and time the secret was created or modified (this is always in UTC) +type ModificationDate = time.Time + +// PinnedProjects The list of projects a user has pinned on their dashboard +type PinnedProjects struct { + ProjectSlugs *[]ProjectSlugResponse `json:"project_slugs,omitempty"` +} + +// ProjectSlug The slug used to identify a project +type ProjectSlug = string + +// ProjectSlugResponse The slug used to identify a project +type ProjectSlugResponse = string + +// SecretDefaultFilename Filename to give to this secret when mounted in Renku 1.0 sessions +type SecretDefaultFilename = string + +// SecretKind Kind of secret +type SecretKind string + +// SecretName The name of a user secret +type SecretName = string + +// SecretPatch Secret metadata to be modified +type SecretPatch struct { + // DefaultFilename Filename to give to this secret when mounted in Renku 1.0 sessions + DefaultFilename *SecretDefaultFilename `json:"default_filename,omitempty"` + + // Name The name of a user secret + Name *SecretName `json:"name,omitempty"` + + // Value Secret value that can be any text + Value *SecretValue `json:"value,omitempty"` +} + +// SecretPost Secret metadata to be created +type SecretPost struct { + // DefaultFilename Filename to give to this secret when mounted in Renku 1.0 sessions + DefaultFilename *SecretDefaultFilename `json:"default_filename,omitempty"` + Kind *SecretKind `json:"kind,omitempty"` + + // Name The name of a user secret + Name SecretName `json:"name"` + + // Value Secret value that can be any text + Value SecretValue `json:"value"` +} + +// SecretValue Secret value that can be any text +type SecretValue = string + +// SecretWithId A Renku secret +type SecretWithId struct { + DataConnectorIds UlidList `json:"data_connector_ids"` + + // DefaultFilename Filename to give to this secret when mounted in Renku 1.0 sessions + DefaultFilename SecretDefaultFilename `json:"default_filename"` + + // Id ULID identifier + Id Ulid `json:"id"` + + // Kind Kind of secret + Kind SecretKind `json:"kind"` + + // ModificationDate The date and time the secret was created or modified (this is always in UTC) + ModificationDate ModificationDate `json:"modification_date"` + + // Name The name of a user secret + Name SecretName `json:"name"` + SessionSecretSlotIds UlidList `json:"session_secret_slot_ids"` +} + +// SecretsList A list of secrets +type SecretsList = []SecretWithId + +// SelfUserInfo Information about the currently logged in user +type SelfUserInfo struct { + // Email User email + Email *UserEmail `json:"email,omitempty"` + + // FirstName First or last name of the user + FirstName *UserFirstLastName `json:"first_name,omitempty"` + + // Id Keycloak user ID + Id UserId `json:"id"` + + // IsAdmin Whether the user is a platform administrator or not + IsAdmin bool `json:"is_admin"` + + // LastName First or last name of the user + LastName *UserFirstLastName `json:"last_name,omitempty"` + + // Username Handle of the user + Username Username `json:"username"` +} + +// ShowProjectMigrationBanner Should display project migration banner +type ShowProjectMigrationBanner = bool + +// Ulid ULID identifier +type Ulid = string + +// UlidList defines model for UlidList. +type UlidList = []Ulid + +// UserEmail User email +type UserEmail = string + +// UserFirstLastName First or last name of the user +type UserFirstLastName = string + +// UserId Keycloak user ID +type UserId = string + +// UserPreferences The object containing user preferences +type UserPreferences struct { + // PinnedProjects The list of projects a user has pinned on their dashboard + PinnedProjects PinnedProjects `json:"pinned_projects"` + + // ShowProjectMigrationBanner Should display project migration banner + ShowProjectMigrationBanner *ShowProjectMigrationBanner `json:"show_project_migration_banner,omitempty"` + + // UserId Keycloak user ID + UserId UserId `json:"user_id"` +} + +// UserSecretKey defines model for UserSecretKey. +type UserSecretKey struct { + // SecretKey The users secret key + SecretKey *string `json:"secret_key,omitempty"` +} + +// UserWithId defines model for UserWithId. +type UserWithId struct { + // Email User email + Email *UserEmail `json:"email,omitempty"` + + // FirstName First or last name of the user + FirstName *UserFirstLastName `json:"first_name,omitempty"` + + // Id Keycloak user ID + Id UserId `json:"id"` + + // LastName First or last name of the user + LastName *UserFirstLastName `json:"last_name,omitempty"` + + // Username Handle of the user + Username Username `json:"username"` +} + +// Username Handle of the user +type Username = string + +// UsersWithId defines model for UsersWithId. +type UsersWithId = []UserWithId + +// Version defines model for Version. +type Version struct { + Version string `json:"version"` +} + +// Error defines model for Error. +type Error = ErrorResponse + +// DeleteUserPreferencesPinnedProjectsParams defines parameters for DeleteUserPreferencesPinnedProjects. +type DeleteUserPreferencesPinnedProjectsParams struct { + // DeletePinnedParams query parameters + DeletePinnedParams *struct { + ProjectSlug *string `json:"project_slug,omitempty"` + } `form:"delete_pinned_params,omitempty" json:"delete_pinned_params,omitempty"` +} + +// GetUserSecretsParams defines parameters for GetUserSecrets. +type GetUserSecretsParams struct { + UserSecretsParams *struct { + // Kind Kind of secret + Kind *SecretKind `json:"kind,omitempty"` + } `form:"user_secrets_params,omitempty" json:"user_secrets_params,omitempty"` +} + +// GetUsersParams defines parameters for GetUsers. +type GetUsersParams struct { + UserParams *struct { + // ExactEmail Return the user(s) with an exact match on the email provided + ExactEmail *string `json:"exact_email,omitempty"` + } `form:"user_params,omitempty" json:"user_params,omitempty"` +} + +// PostUserPreferencesPinnedProjectsJSONRequestBody defines body for PostUserPreferencesPinnedProjects for application/json ContentType. +type PostUserPreferencesPinnedProjectsJSONRequestBody = AddPinnedProject + +// PostUserSecretsJSONRequestBody defines body for PostUserSecrets for application/json ContentType. +type PostUserSecretsJSONRequestBody = SecretPost + +// PatchUserSecretsSecretIdJSONRequestBody defines body for PatchUserSecretsSecretId for application/json ContentType. +type PatchUserSecretsSecretIdJSONRequestBody = SecretPatch + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetError request + GetError(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUser request + GetUser(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUserPreferences request + GetUserPreferences(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteUserPreferencesDismissProjectMigrationBanner request + DeleteUserPreferencesDismissProjectMigrationBanner(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostUserPreferencesDismissProjectMigrationBanner request + PostUserPreferencesDismissProjectMigrationBanner(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteUserPreferencesPinnedProjects request + DeleteUserPreferencesPinnedProjects(ctx context.Context, params *DeleteUserPreferencesPinnedProjectsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostUserPreferencesPinnedProjectsWithBody request with any body + PostUserPreferencesPinnedProjectsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostUserPreferencesPinnedProjects(ctx context.Context, body PostUserPreferencesPinnedProjectsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUserSecretKey request + GetUserSecretKey(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUserSecrets request + GetUserSecrets(ctx context.Context, params *GetUserSecretsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostUserSecretsWithBody request with any body + PostUserSecretsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostUserSecrets(ctx context.Context, body PostUserSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteUserSecretsSecretId request + DeleteUserSecretsSecretId(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUserSecretsSecretId request + GetUserSecretsSecretId(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PatchUserSecretsSecretIdWithBody request with any body + PatchUserSecretsSecretIdWithBody(ctx context.Context, secretId Ulid, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PatchUserSecretsSecretId(ctx context.Context, secretId Ulid, body PatchUserSecretsSecretIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUsers request + GetUsers(ctx context.Context, params *GetUsersParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteUsersUserId request + DeleteUsersUserId(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUsersUserId request + GetUsersUserId(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetVersion request + GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetError(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetErrorRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUser(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUserPreferences(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserPreferencesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteUserPreferencesDismissProjectMigrationBanner(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteUserPreferencesDismissProjectMigrationBannerRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostUserPreferencesDismissProjectMigrationBanner(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostUserPreferencesDismissProjectMigrationBannerRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteUserPreferencesPinnedProjects(ctx context.Context, params *DeleteUserPreferencesPinnedProjectsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteUserPreferencesPinnedProjectsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostUserPreferencesPinnedProjectsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostUserPreferencesPinnedProjectsRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostUserPreferencesPinnedProjects(ctx context.Context, body PostUserPreferencesPinnedProjectsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostUserPreferencesPinnedProjectsRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUserSecretKey(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserSecretKeyRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUserSecrets(ctx context.Context, params *GetUserSecretsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserSecretsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostUserSecretsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostUserSecretsRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostUserSecrets(ctx context.Context, body PostUserSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostUserSecretsRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteUserSecretsSecretId(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteUserSecretsSecretIdRequest(c.Server, secretId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUserSecretsSecretId(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserSecretsSecretIdRequest(c.Server, secretId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PatchUserSecretsSecretIdWithBody(ctx context.Context, secretId Ulid, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPatchUserSecretsSecretIdRequestWithBody(c.Server, secretId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PatchUserSecretsSecretId(ctx context.Context, secretId Ulid, body PatchUserSecretsSecretIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPatchUserSecretsSecretIdRequest(c.Server, secretId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUsers(ctx context.Context, params *GetUsersParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUsersRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteUsersUserId(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteUsersUserIdRequest(c.Server, userId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUsersUserId(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUsersUserIdRequest(c.Server, userId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetVersionRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetErrorRequest generates requests for GetError +func NewGetErrorRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/error") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserRequest generates requests for GetUser +func NewGetUserRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserPreferencesRequest generates requests for GetUserPreferences +func NewGetUserPreferencesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/preferences") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteUserPreferencesDismissProjectMigrationBannerRequest generates requests for DeleteUserPreferencesDismissProjectMigrationBanner +func NewDeleteUserPreferencesDismissProjectMigrationBannerRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/preferences/dismiss_project_migration_banner") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostUserPreferencesDismissProjectMigrationBannerRequest generates requests for PostUserPreferencesDismissProjectMigrationBanner +func NewPostUserPreferencesDismissProjectMigrationBannerRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/preferences/dismiss_project_migration_banner") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteUserPreferencesPinnedProjectsRequest generates requests for DeleteUserPreferencesPinnedProjects +func NewDeleteUserPreferencesPinnedProjectsRequest(server string, params *DeleteUserPreferencesPinnedProjectsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/preferences/pinned_projects") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.DeletePinnedParams != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "delete_pinned_params", runtime.ParamLocationQuery, *params.DeletePinnedParams); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostUserPreferencesPinnedProjectsRequest calls the generic PostUserPreferencesPinnedProjects builder with application/json body +func NewPostUserPreferencesPinnedProjectsRequest(server string, body PostUserPreferencesPinnedProjectsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostUserPreferencesPinnedProjectsRequestWithBody(server, "application/json", bodyReader) +} + +// NewPostUserPreferencesPinnedProjectsRequestWithBody generates requests for PostUserPreferencesPinnedProjects with any type of body +func NewPostUserPreferencesPinnedProjectsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/preferences/pinned_projects") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetUserSecretKeyRequest generates requests for GetUserSecretKey +func NewGetUserSecretKeyRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secret_key") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserSecretsRequest generates requests for GetUserSecrets +func NewGetUserSecretsRequest(server string, params *GetUserSecretsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secrets") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.UserSecretsParams != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "user_secrets_params", runtime.ParamLocationQuery, *params.UserSecretsParams); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostUserSecretsRequest calls the generic PostUserSecrets builder with application/json body +func NewPostUserSecretsRequest(server string, body PostUserSecretsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostUserSecretsRequestWithBody(server, "application/json", bodyReader) +} + +// NewPostUserSecretsRequestWithBody generates requests for PostUserSecrets with any type of body +func NewPostUserSecretsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secrets") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteUserSecretsSecretIdRequest generates requests for DeleteUserSecretsSecretId +func NewDeleteUserSecretsSecretIdRequest(server string, secretId Ulid) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "secret_id", runtime.ParamLocationPath, secretId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secrets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserSecretsSecretIdRequest generates requests for GetUserSecretsSecretId +func NewGetUserSecretsSecretIdRequest(server string, secretId Ulid) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "secret_id", runtime.ParamLocationPath, secretId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secrets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPatchUserSecretsSecretIdRequest calls the generic PatchUserSecretsSecretId builder with application/json body +func NewPatchUserSecretsSecretIdRequest(server string, secretId Ulid, body PatchUserSecretsSecretIdJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPatchUserSecretsSecretIdRequestWithBody(server, secretId, "application/json", bodyReader) +} + +// NewPatchUserSecretsSecretIdRequestWithBody generates requests for PatchUserSecretsSecretId with any type of body +func NewPatchUserSecretsSecretIdRequestWithBody(server string, secretId Ulid, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "secret_id", runtime.ParamLocationPath, secretId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/user/secrets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetUsersRequest generates requests for GetUsers +func NewGetUsersRequest(server string, params *GetUsersParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/users") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.UserParams != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "user_params", runtime.ParamLocationQuery, *params.UserParams); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteUsersUserIdRequest generates requests for DeleteUsersUserId +func NewDeleteUsersUserIdRequest(server string, userId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "user_id", runtime.ParamLocationPath, userId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/users/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUsersUserIdRequest generates requests for GetUsersUserId +func NewGetUsersUserIdRequest(server string, userId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "user_id", runtime.ParamLocationPath, userId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/users/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetVersionRequest generates requests for GetVersion +func NewGetVersionRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/version") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetErrorWithResponse request + GetErrorWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetErrorResponse, error) + + // GetUserWithResponse request + GetUserWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserResponse, error) + + // GetUserPreferencesWithResponse request + GetUserPreferencesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserPreferencesResponse, error) + + // DeleteUserPreferencesDismissProjectMigrationBannerWithResponse request + DeleteUserPreferencesDismissProjectMigrationBannerWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteUserPreferencesDismissProjectMigrationBannerResponse, error) + + // PostUserPreferencesDismissProjectMigrationBannerWithResponse request + PostUserPreferencesDismissProjectMigrationBannerWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostUserPreferencesDismissProjectMigrationBannerResponse, error) + + // DeleteUserPreferencesPinnedProjectsWithResponse request + DeleteUserPreferencesPinnedProjectsWithResponse(ctx context.Context, params *DeleteUserPreferencesPinnedProjectsParams, reqEditors ...RequestEditorFn) (*DeleteUserPreferencesPinnedProjectsResponse, error) + + // PostUserPreferencesPinnedProjectsWithBodyWithResponse request with any body + PostUserPreferencesPinnedProjectsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUserPreferencesPinnedProjectsResponse, error) + + PostUserPreferencesPinnedProjectsWithResponse(ctx context.Context, body PostUserPreferencesPinnedProjectsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUserPreferencesPinnedProjectsResponse, error) + + // GetUserSecretKeyWithResponse request + GetUserSecretKeyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserSecretKeyResponse, error) + + // GetUserSecretsWithResponse request + GetUserSecretsWithResponse(ctx context.Context, params *GetUserSecretsParams, reqEditors ...RequestEditorFn) (*GetUserSecretsResponse, error) + + // PostUserSecretsWithBodyWithResponse request with any body + PostUserSecretsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUserSecretsResponse, error) + + PostUserSecretsWithResponse(ctx context.Context, body PostUserSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUserSecretsResponse, error) + + // DeleteUserSecretsSecretIdWithResponse request + DeleteUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*DeleteUserSecretsSecretIdResponse, error) + + // GetUserSecretsSecretIdWithResponse request + GetUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*GetUserSecretsSecretIdResponse, error) + + // PatchUserSecretsSecretIdWithBodyWithResponse request with any body + PatchUserSecretsSecretIdWithBodyWithResponse(ctx context.Context, secretId Ulid, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchUserSecretsSecretIdResponse, error) + + PatchUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, body PatchUserSecretsSecretIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PatchUserSecretsSecretIdResponse, error) + + // GetUsersWithResponse request + GetUsersWithResponse(ctx context.Context, params *GetUsersParams, reqEditors ...RequestEditorFn) (*GetUsersResponse, error) + + // DeleteUsersUserIdWithResponse request + DeleteUsersUserIdWithResponse(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*DeleteUsersUserIdResponse, error) + + // GetUsersUserIdWithResponse request + GetUsersUserIdWithResponse(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*GetUsersUserIdResponse, error) + + // GetVersionWithResponse request + GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) +} + +type GetErrorResponse struct { + Body []byte + HTTPResponse *http.Response + JSON422 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetErrorResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetErrorResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SelfUserInfo + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUserResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserPreferencesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPreferences + JSON404 *ErrorResponse + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUserPreferencesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserPreferencesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteUserPreferencesDismissProjectMigrationBannerResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPreferences + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteUserPreferencesDismissProjectMigrationBannerResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteUserPreferencesDismissProjectMigrationBannerResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostUserPreferencesDismissProjectMigrationBannerResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPreferences + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r PostUserPreferencesDismissProjectMigrationBannerResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostUserPreferencesDismissProjectMigrationBannerResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteUserPreferencesPinnedProjectsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPreferences + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteUserPreferencesPinnedProjectsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteUserPreferencesPinnedProjectsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostUserPreferencesPinnedProjectsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPreferences + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r PostUserPreferencesPinnedProjectsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostUserPreferencesPinnedProjectsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserSecretKeyResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserSecretKey + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUserSecretKeyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserSecretKeyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserSecretsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SecretsList + JSON404 *ErrorResponse + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUserSecretsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserSecretsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostUserSecretsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *SecretWithId + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r PostUserSecretsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostUserSecretsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteUserSecretsSecretIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteUserSecretsSecretIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteUserSecretsSecretIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserSecretsSecretIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SecretWithId + JSON404 *ErrorResponse + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUserSecretsSecretIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserSecretsSecretIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PatchUserSecretsSecretIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *SecretWithId + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r PatchUserSecretsSecretIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PatchUserSecretsSecretIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUsersResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UsersWithId + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUsersResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUsersResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteUsersUserIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteUsersUserIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteUsersUserIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUsersUserIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserWithId + JSON404 *ErrorResponse + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetUsersUserIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUsersUserIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetVersionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Version +} + +// Status returns HTTPResponse.Status +func (r GetVersionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetVersionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetErrorWithResponse request returning *GetErrorResponse +func (c *ClientWithResponses) GetErrorWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetErrorResponse, error) { + rsp, err := c.GetError(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetErrorResponse(rsp) +} + +// GetUserWithResponse request returning *GetUserResponse +func (c *ClientWithResponses) GetUserWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserResponse, error) { + rsp, err := c.GetUser(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserResponse(rsp) +} + +// GetUserPreferencesWithResponse request returning *GetUserPreferencesResponse +func (c *ClientWithResponses) GetUserPreferencesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserPreferencesResponse, error) { + rsp, err := c.GetUserPreferences(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserPreferencesResponse(rsp) +} + +// DeleteUserPreferencesDismissProjectMigrationBannerWithResponse request returning *DeleteUserPreferencesDismissProjectMigrationBannerResponse +func (c *ClientWithResponses) DeleteUserPreferencesDismissProjectMigrationBannerWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteUserPreferencesDismissProjectMigrationBannerResponse, error) { + rsp, err := c.DeleteUserPreferencesDismissProjectMigrationBanner(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteUserPreferencesDismissProjectMigrationBannerResponse(rsp) +} + +// PostUserPreferencesDismissProjectMigrationBannerWithResponse request returning *PostUserPreferencesDismissProjectMigrationBannerResponse +func (c *ClientWithResponses) PostUserPreferencesDismissProjectMigrationBannerWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostUserPreferencesDismissProjectMigrationBannerResponse, error) { + rsp, err := c.PostUserPreferencesDismissProjectMigrationBanner(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostUserPreferencesDismissProjectMigrationBannerResponse(rsp) +} + +// DeleteUserPreferencesPinnedProjectsWithResponse request returning *DeleteUserPreferencesPinnedProjectsResponse +func (c *ClientWithResponses) DeleteUserPreferencesPinnedProjectsWithResponse(ctx context.Context, params *DeleteUserPreferencesPinnedProjectsParams, reqEditors ...RequestEditorFn) (*DeleteUserPreferencesPinnedProjectsResponse, error) { + rsp, err := c.DeleteUserPreferencesPinnedProjects(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteUserPreferencesPinnedProjectsResponse(rsp) +} + +// PostUserPreferencesPinnedProjectsWithBodyWithResponse request with arbitrary body returning *PostUserPreferencesPinnedProjectsResponse +func (c *ClientWithResponses) PostUserPreferencesPinnedProjectsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUserPreferencesPinnedProjectsResponse, error) { + rsp, err := c.PostUserPreferencesPinnedProjectsWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostUserPreferencesPinnedProjectsResponse(rsp) +} + +func (c *ClientWithResponses) PostUserPreferencesPinnedProjectsWithResponse(ctx context.Context, body PostUserPreferencesPinnedProjectsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUserPreferencesPinnedProjectsResponse, error) { + rsp, err := c.PostUserPreferencesPinnedProjects(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostUserPreferencesPinnedProjectsResponse(rsp) +} + +// GetUserSecretKeyWithResponse request returning *GetUserSecretKeyResponse +func (c *ClientWithResponses) GetUserSecretKeyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserSecretKeyResponse, error) { + rsp, err := c.GetUserSecretKey(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserSecretKeyResponse(rsp) +} + +// GetUserSecretsWithResponse request returning *GetUserSecretsResponse +func (c *ClientWithResponses) GetUserSecretsWithResponse(ctx context.Context, params *GetUserSecretsParams, reqEditors ...RequestEditorFn) (*GetUserSecretsResponse, error) { + rsp, err := c.GetUserSecrets(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserSecretsResponse(rsp) +} + +// PostUserSecretsWithBodyWithResponse request with arbitrary body returning *PostUserSecretsResponse +func (c *ClientWithResponses) PostUserSecretsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUserSecretsResponse, error) { + rsp, err := c.PostUserSecretsWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostUserSecretsResponse(rsp) +} + +func (c *ClientWithResponses) PostUserSecretsWithResponse(ctx context.Context, body PostUserSecretsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUserSecretsResponse, error) { + rsp, err := c.PostUserSecrets(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostUserSecretsResponse(rsp) +} + +// DeleteUserSecretsSecretIdWithResponse request returning *DeleteUserSecretsSecretIdResponse +func (c *ClientWithResponses) DeleteUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*DeleteUserSecretsSecretIdResponse, error) { + rsp, err := c.DeleteUserSecretsSecretId(ctx, secretId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteUserSecretsSecretIdResponse(rsp) +} + +// GetUserSecretsSecretIdWithResponse request returning *GetUserSecretsSecretIdResponse +func (c *ClientWithResponses) GetUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, reqEditors ...RequestEditorFn) (*GetUserSecretsSecretIdResponse, error) { + rsp, err := c.GetUserSecretsSecretId(ctx, secretId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserSecretsSecretIdResponse(rsp) +} + +// PatchUserSecretsSecretIdWithBodyWithResponse request with arbitrary body returning *PatchUserSecretsSecretIdResponse +func (c *ClientWithResponses) PatchUserSecretsSecretIdWithBodyWithResponse(ctx context.Context, secretId Ulid, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchUserSecretsSecretIdResponse, error) { + rsp, err := c.PatchUserSecretsSecretIdWithBody(ctx, secretId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePatchUserSecretsSecretIdResponse(rsp) +} + +func (c *ClientWithResponses) PatchUserSecretsSecretIdWithResponse(ctx context.Context, secretId Ulid, body PatchUserSecretsSecretIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PatchUserSecretsSecretIdResponse, error) { + rsp, err := c.PatchUserSecretsSecretId(ctx, secretId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePatchUserSecretsSecretIdResponse(rsp) +} + +// GetUsersWithResponse request returning *GetUsersResponse +func (c *ClientWithResponses) GetUsersWithResponse(ctx context.Context, params *GetUsersParams, reqEditors ...RequestEditorFn) (*GetUsersResponse, error) { + rsp, err := c.GetUsers(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUsersResponse(rsp) +} + +// DeleteUsersUserIdWithResponse request returning *DeleteUsersUserIdResponse +func (c *ClientWithResponses) DeleteUsersUserIdWithResponse(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*DeleteUsersUserIdResponse, error) { + rsp, err := c.DeleteUsersUserId(ctx, userId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteUsersUserIdResponse(rsp) +} + +// GetUsersUserIdWithResponse request returning *GetUsersUserIdResponse +func (c *ClientWithResponses) GetUsersUserIdWithResponse(ctx context.Context, userId string, reqEditors ...RequestEditorFn) (*GetUsersUserIdResponse, error) { + rsp, err := c.GetUsersUserId(ctx, userId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUsersUserIdResponse(rsp) +} + +// GetVersionWithResponse request returning *GetVersionResponse +func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) { + rsp, err := c.GetVersion(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetVersionResponse(rsp) +} + +// ParseGetErrorResponse parses an HTTP response from a GetErrorWithResponse call +func ParseGetErrorResponse(rsp *http.Response) (*GetErrorResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetErrorResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + } + + return response, nil +} + +// ParseGetUserResponse parses an HTTP response from a GetUserWithResponse call +func ParseGetUserResponse(rsp *http.Response) (*GetUserResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SelfUserInfo + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUserPreferencesResponse parses an HTTP response from a GetUserPreferencesWithResponse call +func ParseGetUserPreferencesResponse(rsp *http.Response) (*GetUserPreferencesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserPreferencesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserPreferences + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteUserPreferencesDismissProjectMigrationBannerResponse parses an HTTP response from a DeleteUserPreferencesDismissProjectMigrationBannerWithResponse call +func ParseDeleteUserPreferencesDismissProjectMigrationBannerResponse(rsp *http.Response) (*DeleteUserPreferencesDismissProjectMigrationBannerResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteUserPreferencesDismissProjectMigrationBannerResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserPreferences + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParsePostUserPreferencesDismissProjectMigrationBannerResponse parses an HTTP response from a PostUserPreferencesDismissProjectMigrationBannerWithResponse call +func ParsePostUserPreferencesDismissProjectMigrationBannerResponse(rsp *http.Response) (*PostUserPreferencesDismissProjectMigrationBannerResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostUserPreferencesDismissProjectMigrationBannerResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserPreferences + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteUserPreferencesPinnedProjectsResponse parses an HTTP response from a DeleteUserPreferencesPinnedProjectsWithResponse call +func ParseDeleteUserPreferencesPinnedProjectsResponse(rsp *http.Response) (*DeleteUserPreferencesPinnedProjectsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteUserPreferencesPinnedProjectsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserPreferences + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParsePostUserPreferencesPinnedProjectsResponse parses an HTTP response from a PostUserPreferencesPinnedProjectsWithResponse call +func ParsePostUserPreferencesPinnedProjectsResponse(rsp *http.Response) (*PostUserPreferencesPinnedProjectsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostUserPreferencesPinnedProjectsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserPreferences + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUserSecretKeyResponse parses an HTTP response from a GetUserSecretKeyWithResponse call +func ParseGetUserSecretKeyResponse(rsp *http.Response) (*GetUserSecretKeyResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserSecretKeyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserSecretKey + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUserSecretsResponse parses an HTTP response from a GetUserSecretsWithResponse call +func ParseGetUserSecretsResponse(rsp *http.Response) (*GetUserSecretsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserSecretsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SecretsList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParsePostUserSecretsResponse parses an HTTP response from a PostUserSecretsWithResponse call +func ParsePostUserSecretsResponse(rsp *http.Response) (*PostUserSecretsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostUserSecretsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest SecretWithId + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteUserSecretsSecretIdResponse parses an HTTP response from a DeleteUserSecretsSecretIdWithResponse call +func ParseDeleteUserSecretsSecretIdResponse(rsp *http.Response) (*DeleteUserSecretsSecretIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteUserSecretsSecretIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUserSecretsSecretIdResponse parses an HTTP response from a GetUserSecretsSecretIdWithResponse call +func ParseGetUserSecretsSecretIdResponse(rsp *http.Response) (*GetUserSecretsSecretIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserSecretsSecretIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SecretWithId + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParsePatchUserSecretsSecretIdResponse parses an HTTP response from a PatchUserSecretsSecretIdWithResponse call +func ParsePatchUserSecretsSecretIdResponse(rsp *http.Response) (*PatchUserSecretsSecretIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PatchUserSecretsSecretIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest SecretWithId + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUsersResponse parses an HTTP response from a GetUsersWithResponse call +func ParseGetUsersResponse(rsp *http.Response) (*GetUsersResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUsersResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UsersWithId + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteUsersUserIdResponse parses an HTTP response from a DeleteUsersUserIdWithResponse call +func ParseDeleteUsersUserIdResponse(rsp *http.Response) (*DeleteUsersUserIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteUsersUserIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetUsersUserIdResponse parses an HTTP response from a GetUsersUserIdWithResponse call +func ParseGetUsersUserIdResponse(rsp *http.Response) (*GetUsersUserIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUsersUserIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserWithId + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetVersionResponse parses an HTTP response from a GetVersionWithResponse call +func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetVersionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Version + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+xabXPbNhL+KxhcZ5q0pCUrTtPoy51bt1e3Sc+Tt87E1mkgYiWiJgEWAG2rHv33G7zw", + "TaRkKpbTNHOfbJEEdrH77INdLG5xJNJMcOBa4fEtlqAywRXYHz9IKaT5JxJcA9fmX5JlCYuIZoIPfleC", + "m2cqiiEl5r8vJMzxGP9jUM06cG/VwM72ys+PV6tVgCmoSLLMTIbH+E0MyH2M5kIikiTo6OYGEU7R05sb", + "VKlmhvpZjdBjSs8Y50DPpPgdIqcmpcxMS5IzKTKQmpklzUmiIMBZ7dGt+WVGTVWSL+5ag5fw2nxqtJDw", + "R84kUDw+b84zCbBeZoDHWMysTqsANw3gJNf0gMLazceRoPZjuCFplgAeHx4NjwIMN1GSK3YFLxlnaZ7i", + "sZY5BDgtfg5LDRjXsACJrcE1YUljOnyMUiEBuVdAkcic4VAKSpEFIBWLa8YX6DomGukYUCbFLIEUXROF", + "SylKS8YXRogf15TyWqSgYzsNcI2upeALFKIsAaIAablEZEEYRwnRINuzrtnaGqWS1Lb22vfOtl1OeSko", + "m3tAnxBttW7DkhINFoiapWBtoCCSoI0JUCSBaGM4iVI7G1D0SMdMIaYQSa7JUiHG0ds33z/GQc0ko+Ho", + "SXh4GA4P3xw+Gz8ZjUffvscBnguZEo3H2AgNjcAuIzcQr7q1TpjSSMyRR6ZCBOUKJIqJQpkdjwQ3q2ES", + "UaLimSCS4m3xYR8wDanaIVKqmC/XQaQkS+ullkPqIda5KqOHWQdFWiBGgWs2XyJSrLJhYrPcQboMq3cp", + "4y+AL3SMx08CnBGtQZqZ/3tOwj+Pw/fD8Pnk60fn04OLi3AwqT98/FXx9J+1x190Oqdj8Q++lpYWry1E", + "T2BO8kT/yBLgJO3Qo3hjVFiwK/vXoreAeAwcpSLnBuOMo1fAL3N0eDBECpRigqsL3lD0hGgSvn4SOvnT", + "Q6MpuSk0ff68ofjhBidMLy7Cg8lXneZ1E//COG2vxjw1mHfKG724YcNzvAAOkiQ4wEoL2WSN9al/7TSU", + "cZg1lJgXkVRJqej07BS9EZfA71r2BtlnREfxnVtYUzM3EqWgCSWaGA/OoCSjVkRTB4npvIaJbbHcDaRV", + "gPsP/tWPuCJJ3nPIO/tpJ0d4Swml92IoT+APZ6dLD1WSJP+Z4/F5n2ksvFeToNACj0sIfzzL1/dQK7KY", + "Z7LRK+8KOZ2Wt6ORNplERLgxPuFLpOFGN8Pl6XA47BkwvzEdn9IdgXDsaawM4DXPE02mkeAcIi3klNE7", + "97u3CaMvmNIuy9oTbhjtI7aOsL64CnBaS3ym1Gc+24a3MqUPgqHfM6bO8FOVCL2beddQyUzYemi27N61", + "Sm+rzZoEXd7fjHdl1Wrh/bjMvtz8Zt5eiVMD1K2MyUhN5m8VyFM+FzuC3gwxuSUTHJGZyF02H+VSAtfJ", + "EiVisXB7vNncWkEBqa8dtjpKgfzBfrgK8JxJpad9QGKG/Wi+fkFUiZUe8DeGsGZiakpoyniNtzdY4bcY", + "dAzSrt1u4iZLR1lCtLEOsrMwpSXRQpqcngtdJSEzIRIg3EhMyH3WZiT3HezooAv45Sw1A3RCNRbXPil9", + "yRbSQuA7wjnIDqaORZ5QRJnKErIsUlGUFuPQzA3ssomlo9aMb1+cnhTZLbMja0w/+qbB8+ZnPSEchs8m", + "58Pw+XH408+/vPz1LHzzLnw/uR09XXWmhiVP9K1TCgJdD7MKx+3lGNC4YKgnfkqkEBqH/Gth3h1EIu3U", + "sIWGjoxcGuqQyECsTDkLvDaE/izitURz9PSbOzdOHzbt5BmWUSLIpYuL05OGqPmzIzIafTsLD589H4ZH", + "o2ffhGT+dB6Onkajo9ERPJ8NI9x03nH4noR/mjLp9nBV/QonX3+xyTRnEuYggUeOc3ZgN5OeO8SjSHBN", + "GGd84VaS1SZtlbe2EJ5mtUp6a13brLvNjhaL62L4tIyS6awMr61svzkwPUlM+7PgGkMUo4PWGrsowkzi", + "MwRY7niE5rfQSzew7RajSVlPmq+6znc6NeqZ3JUovW3sOfhnwi030iI+JeFUpOGlB3pozVNjcnwioM6r", + "tbAukrK/0ab4SW1Rm0DXfSrxE+E02cx7Ta/sWmkbsaqCVr+NokLj+nYR4JyzP3I4dfNomcMqwO9AKuYO", + "yJuQuapebD/lLD7sPN5UEOWS6eVro5+bWDAa2b8Z8FP6vUtg38oEj/GA5DoeSCBJqga29BkcXEOShJdc", + "XPOBGcFoGAk+Z4vc0VAVpI353Mk989nneqzbYyN5xSIwaZVxXUqYSRmiS+DUnutb6QfoVJvU4opRUIi1", + "ElPLGEF5chlc8CgROUX+7CZAJIpAKVPAG1/lGpAEJXIZgbIHtampK4VP9BhfqAN7SqWZtvhx1d8J0QS9", + "dvoqdHx2aurbwjv46tB42iyeZAyP8ZOD4cHI7XCxtfegPLJfgPaGd7YzuML/Bu06KEGzrXI0Gn3cpopT", + "04ImT1Mil045RJCy8eQ+KBss6JrpGClNdK5QJCggo7EZPrBxuGW9b12cNpY7Gg73ttxG8bNhtRvrmVXt", + "LKVbTKm3s2uHydiuJZQmC1VsxgpPSisOsmais82iZ4305cGMuy5qg31bGdUqwEfDo48L6bKPwUVdF6TI", + "FdC9eHp9mZa7dnD2tG6ibr8PKFMpU2pr9kghAXc408TGiX2+5rMTN+GGjPITwE5GbausC0P38tgrSMUV", + "IG9QX6K2a9ftPgpw5o+Um6Y+E0r/39DO0Cf3snBnFHRUYDuBfq0kMxu0JCloQ7jj8/Uc5Y8c5BLVPjFp", + "ZZbYLrtrobPys+JocewVmhaqmtFmZOWze1w4qA74+9VFSi9dQS5kajsEn3tUCw7IXwjxbeuscvZ+4rmF", + "IZOHg9LfCbrcmzlbF1VWzYzfFw6frTuPKUVkzYU9+aJ5wrAtWarOMB7YkpWgTXeZyvOOoozdlDocXHBb", + "OgGnmWBcm9qJC42yfJawKFn6aofNEjBlzD7Sm5o6nccz3idF86LlCtXPDx103INtLRK8nHuS7a6Nsb+W", + "cOstpS2Z75eq7Cr9Zck3FeBACjfNtueHYdIs2TK8X5i7AuhEPbqEpUKCJ8vHndC8i+crKD4Eq9cuI/Ti", + "88M9Sy4Opdq+8j13lVv2mOdJsizvOtzXX9/beRBBHK4L6igqpLVqaDOHDG49rzO66pf2eVe6P6e0zS6W", + "TjKi44pNShl43TlBX7q3TaKOyD/acK2ruhzolmQvB1JG+Zf7ihdnEkSqGwxdcdGDoD8VUw4/WlhUHvps", + "2NOf5lUJx2yJrJM6ybK4YbbGlubxpwGMByNpu/K/A0vHhC/2wdJ2nu0sUZDynRndPXK5++VwcEMiPYXu", + "bvwr0Lnk5b7zSD1259eEIzsOpcbp/qKza9sXjQf6KdbbajtxFbeKXM7OuL+M7lou1a1zpPKZAvulvbbP", + "7LdFf//x/tK13ANj0zm3Gtz6PnTPHV759mUf1qk63Js5Z93BvTdxy9NmC29EZnM/3yOFVxt6BhGbs8gp", + "MFv6C/rl3Qx7LWPd3Nu3+o9v0/3GxPaQ8PuFP7H43Pb0D0GDCb5al3kTNIoO9QN6rxCxY2fSkJpfQHF2", + "4jnOzVQ0vy2IXdv7HLsmNp4YOJqvC5DnvgOesQElmuDVZPW/AAAA//+0kY9y9zYAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go new file mode 100644 index 0000000..a276b18 --- /dev/null +++ b/pkg/tools/tools.go @@ -0,0 +1,5 @@ +package tools + +import ( + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +) From db1fb01f8b0c5e81786d106e68b89cd90e9d47d9 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 23 Sep 2025 14:24:33 +0200 Subject: [PATCH 07/10] add logout --- pkg/cmd/logout.go | 89 ++++++++++++++++++++++++++++++++++++++++++++ pkg/cmd/root.go | 1 + pkg/renkuapi/auth.go | 27 ++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 pkg/cmd/logout.go diff --git a/pkg/cmd/logout.go b/pkg/cmd/logout.go new file mode 100644 index 0000000..57f7eef --- /dev/null +++ b/pkg/cmd/logout.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/github" + ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace" + "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/renkuapi" + "github.com/spf13/cobra" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Log out of a renku instance", + Run: logout, +} + +func logout(cmd *cobra.Command, args []string) { + ctx := context.Background() + + logoutAll, err := cmd.Flags().GetBool("all") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if logoutAll { + err := renkuapi.LogoutAll(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return + } + + url, err := cmd.Flags().GetString("url") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if url == "" { + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if namespace == "" { + cli, err := github.NewGitHubCLI("") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + namespace, err = ns.FindCurrentNamespace(ctx, cli) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + + deploymentURL, err := ns.GetDeploymentURL(namespace) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + url = deploymentURL.String() + } + + fmt.Printf("Renku URL: %s\n", url) + + auth, err := renkuapi.NewRenkuApiAuth(url) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = auth.Logout(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + logoutCmd.Flags().String("url", "", "instance URL") + logoutCmd.Flags().StringP("namespace", "n", "", "k8s namespace") + logoutCmd.Flags().Bool("all", false, "remote all saved logins") +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index fe3ae34..ba2a9ec 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -34,6 +34,7 @@ func init() { rootCmd.AddCommand(cleanupDeploymentCmd) rootCmd.AddCommand(copyKeycloakAdminPasswordCmd) rootCmd.AddCommand(loginCmd) + rootCmd.AddCommand(logoutCmd) rootCmd.AddCommand(makeMeAdminCmd) rootCmd.AddCommand(namespaceCmd) rootCmd.AddCommand(openDeploymentCmd) diff --git a/pkg/renkuapi/auth.go b/pkg/renkuapi/auth.go index d920fb1..b74af5d 100644 --- a/pkg/renkuapi/auth.go +++ b/pkg/renkuapi/auth.go @@ -155,6 +155,11 @@ func (auth *RenkuApiAuth) saveAccessTokenToKeyring() (err error) { return keyring.Set(keyringService, kUser, auth.accessToken) } +func (auth *RenkuApiAuth) deleteAccessTokenFromKeyring() (err error) { + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token") + return keyring.Delete(keyringService, kUser) +} + func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) { kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token") return keyring.Get(keyringService, kUser) @@ -168,6 +173,11 @@ func (auth *RenkuApiAuth) saveRefreshTokenToKeyring() (err error) { return keyring.Set(keyringService, kUser, auth.refreshToken) } +func (auth *RenkuApiAuth) deleteRefreshTokenFromKeyring() (err error) { + kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token") + return keyring.Delete(keyringService, kUser) +} + func (auth *RenkuApiAuth) getKeyringUserPrefix() string { return fmt.Sprintf("rdu:%s", auth.baseURL.String()) } @@ -475,3 +485,20 @@ func (auth *RenkuApiAuth) postRefeshToken(ctx context.Context, refreshToken stri } return result, nil } + +func (auth *RenkuApiAuth) Logout(ctx context.Context) error { + err1 := auth.deleteAccessTokenFromKeyring() + err2 := auth.deleteRefreshTokenFromKeyring() + if err1 != nil && err2 != nil { + return fmt.Errorf("got errors: %w and %w", err1, err2) + } + if err1 != nil { + return err1 + } + return err2 +} + +func LogoutAll(ctx context.Context) error { + return keyring.DeleteAll(keyringService) + +} From 2ddae5205e8fcfb484973eb6d53fae0fb3000c9f Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 23 Sep 2025 14:30:58 +0200 Subject: [PATCH 08/10] tool --- go.mod | 4 +++- pkg/tools/tools.go | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 pkg/tools/tools.go diff --git a/go.mod b/go.mod index 26acbdd..ac8c87d 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,11 @@ module github.com/SwissDataScienceCenter/renku-dev-utils go 1.24.2 +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + require ( github.com/getkin/kin-openapi v0.132.0 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 github.com/oapi-codegen/runtime v1.1.2 github.com/spf13/cobra v1.10.1 github.com/zalando/go-keyring v0.2.6 @@ -36,6 +37,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go deleted file mode 100644 index a276b18..0000000 --- a/pkg/tools/tools.go +++ /dev/null @@ -1,5 +0,0 @@ -package tools - -import ( - _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" -) From 99d06cd3353ca7b8969fe8db42e05a0dcc40b639 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 23 Sep 2025 14:33:26 +0200 Subject: [PATCH 09/10] typo --- pkg/cmd/logout.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/logout.go b/pkg/cmd/logout.go index 57f7eef..29527c6 100644 --- a/pkg/cmd/logout.go +++ b/pkg/cmd/logout.go @@ -85,5 +85,5 @@ func logout(cmd *cobra.Command, args []string) { func init() { logoutCmd.Flags().String("url", "", "instance URL") logoutCmd.Flags().StringP("namespace", "n", "", "k8s namespace") - logoutCmd.Flags().Bool("all", false, "remote all saved logins") + logoutCmd.Flags().Bool("all", false, "remove all saved logins") } From 5c051def793d8c7369ce5259f1cb456a7523e796 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 23 Sep 2025 14:35:29 +0200 Subject: [PATCH 10/10] format --- pkg/cmd/login.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/login.go b/pkg/cmd/login.go index 0bcdbf8..2172403 100644 --- a/pkg/cmd/login.go +++ b/pkg/cmd/login.go @@ -86,7 +86,6 @@ func login(cmd *cobra.Command, args []string) { fmt.Printf(" first name: %s\n", *userInfo.FirstName) fmt.Printf(" last name: %s\n", *userInfo.LastName) fmt.Printf(" is admin: %t\n", userInfo.IsAdmin) - } func init() {