diff --git a/cmd/hzctl/delete.go b/cmd/hzctl/delete.go index aea07ef..0907abf 100644 --- a/cmd/hzctl/delete.go +++ b/cmd/hzctl/delete.go @@ -13,6 +13,7 @@ import ( type deleteCmdOptions struct { filename string + key string } var deleteOpts deleteCmdOptions @@ -39,18 +40,31 @@ to quickly create a Cobra application.`, ) } - if deleteOpts.filename == "" { - return fmt.Errorf("filename is required") - } + clientDeleteOpts := []hz.HTTPDeleteOption{} + if deleteOpts.filename != "" { + yData, err := os.ReadFile(deleteOpts.filename) + if err != nil { + return fmt.Errorf("open file: %w", err) + } - yData, err := os.ReadFile(deleteOpts.filename) - if err != nil { - return fmt.Errorf("open file: %w", err) + jData, err := yaml.YAMLToJSONStrict(yData) + if err != nil { + return fmt.Errorf("convert yaml to json: %w", err) + } + clientDeleteOpts = append( + clientDeleteOpts, + hz.WithHTTPDeleteData(jData), + ) } - - jData, err := yaml.YAMLToJSONStrict(yData) - if err != nil { - return fmt.Errorf("convert yaml to json: %w", err) + if deleteOpts.key != "" { + objKey, err := hz.ObjectKeyFromString(deleteOpts.key) + if err != nil { + return fmt.Errorf("parse key: %w", err) + } + clientDeleteOpts = append( + clientDeleteOpts, + hz.WithHTTPDeleteKey(objKey), + ) } client := hz.HTTPClient{ @@ -60,7 +74,7 @@ to quickly create a Cobra application.`, } ctx := context.Background() - if err := client.Delete(ctx, hz.WithHTTPDeleteData(jData)); err != nil { + if err := client.Delete(ctx, clientDeleteOpts...); err != nil { return fmt.Errorf("delete: %w", err) } @@ -79,4 +93,11 @@ func init() { "", "Filename to delete", ) + flags.StringVarP( + &deleteOpts.key, + "key", + "k", + "", + "Key to delete", + ) } diff --git a/cmd/hzctl/login.go b/cmd/hzctl/login.go index 096afe7..44560f8 100644 --- a/cmd/hzctl/login.go +++ b/cmd/hzctl/login.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/base64" "fmt" "os" @@ -36,10 +35,6 @@ to quickly create a Cobra application.`, if err != nil { return err } - creds := base64.StdEncoding.EncodeToString( - []byte(resp.Credentials), - ) - hCtx.Credentials = &creds hCtx.Session = &resp.Session config.Add(hCtx) f, err := os.Create(configFile) diff --git a/examples/greetings/cmd/main.go b/examples/greetings/cmd/main.go index 6b342b3..b8637db 100644 --- a/examples/greetings/cmd/main.go +++ b/examples/greetings/cmd/main.go @@ -13,11 +13,9 @@ import ( ) func main() { - // TODO: get NATS credentials and server information. if err := run(); err != nil { slog.Error("running", "error", err) os.Exit(1) - } } diff --git a/examples/greetings/examples_test.go b/examples/greetings/examples_test.go index bbdd6d9..99058cc 100644 --- a/examples/greetings/examples_test.go +++ b/examples/greetings/examples_test.go @@ -112,7 +112,7 @@ func TestGreeting(t *testing.T) { select { case <-timeout: - t.Fatal("timed out waiting for account") + t.Fatal("timed out waiting for greeting") case <-done: } } diff --git a/examples/greetings/portal.go b/examples/greetings/portal.go index 40663f2..e831e9d 100644 --- a/examples/greetings/portal.go +++ b/examples/greetings/portal.go @@ -33,7 +33,7 @@ type PortalHandler struct { func (h *PortalHandler) Router() *chi.Mux { r := chi.NewRouter() - logger := httplog.NewLogger("horizon", httplog.Options{ + logger := httplog.NewLogger("portal-greetings", httplog.Options{ JSON: false, LogLevel: slog.LevelInfo, Concise: true, diff --git a/go.mod b/go.mod index ffc89f1..9ffb33f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,10 @@ toolchain go1.21.6 require ( cuelang.org/go v0.7.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 github.com/a-h/templ v0.2.543 github.com/charmbracelet/huh v0.3.0 github.com/charmbracelet/lipgloss v0.9.1 @@ -19,6 +23,8 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 + github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 + github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 github.com/nats-io/jwt/v2 v2.5.5 github.com/nats-io/nats-server/v2 v2.10.11 github.com/nats-io/nats.go v1.33.1 @@ -44,6 +50,7 @@ require ( github.com/Antonboom/errname v0.1.12 // indirect github.com/Antonboom/nilnil v0.1.7 // indirect github.com/Antonboom/testifylint v1.1.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect @@ -106,6 +113,7 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.8.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect @@ -136,6 +144,7 @@ require ( github.com/klauspost/compress v1.17.7 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.9 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect github.com/ldez/gomoddirectives v0.2.3 // indirect github.com/ldez/tagliatelle v0.5.0 // indirect @@ -175,6 +184,7 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.4.8 // indirect github.com/prometheus/client_golang v1.12.1 // indirect diff --git a/go.sum b/go.sum index 5476f8a..95bdf8e 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,22 @@ github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTo github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= github.com/Antonboom/testifylint v1.1.2 h1:IdLRermiLRogxY5AumBL4sP0A+qKHQM/AP1Xd7XOTKc= github.com/Antonboom/testifylint v1.1.2/go.mod h1:9PFi+vWa8zzl4/B/kqmFJcw85ZUv8ReyBzuQCd30+WI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 h1:UPeCRD+XY7QlaGQte2EVI2iOcWvUYA2XY8w5T/8v0NQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1/go.mod h1:oGV6NlB0cvi1ZbYRR2UN44QHxWFyGk+iylgD0qaMXjA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 h1:nBy98uKOIfun5z6wx6jwWLrULcM0+cjBalBFZlEZ7CA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -164,6 +180,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -245,6 +263,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -328,6 +348,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -401,6 +422,8 @@ github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= github.com/kunwardeep/paralleltest v1.0.9 h1:3Sr2IfFNcsMmlqPk1cjTUbJ4zofKPGyHxenwPebgTug= github.com/kunwardeep/paralleltest v1.0.9/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= @@ -444,6 +467,10 @@ github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwg github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= +github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU= +github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -516,6 +543,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/pkg/gateway/accounts.go b/pkg/gateway/accounts.go index 7d58519..0689302 100644 --- a/pkg/gateway/accounts.go +++ b/pkg/gateway/accounts.go @@ -183,9 +183,23 @@ func (h *AccountsHandler) servePortal( account: account, } proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + // NOTE: this only handles errors returned from the proxy. + // I.e. if an HTTP response is received, then it is not considered + // an error. w.WriteHeader(http.StatusOK) _ = portalError(err).Render(r.Context(), w) } + // This is one idea to handle errors returned from portals. + // Ideally portals should only return 2xx status codes, as per the + // HATEOS way of handling things. + // https://htmx.org/essays/hateoas/ + // proxy.ModifyResponse = func(resp *http.Response) error { + // // Modify the response if the status code is not 2xx. + // if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // // Modify the response here to be 2xx for HTMX to render it. + // } + // return nil + // } proxy.ServeHTTP(w, r) return } diff --git a/pkg/gateway/gateway.templ b/pkg/gateway/gateway.templ index 38b0b6e..519761b 100644 --- a/pkg/gateway/gateway.templ +++ b/pkg/gateway/gateway.templ @@ -1,7 +1,7 @@ package gateway import ( - "fmt" + "strings" "github.com/verifa/horizon/pkg/hz" "github.com/verifa/horizon/pkg/auth" ) @@ -78,12 +78,18 @@ templ nav(userInfo *auth.UserInfo) { } -templ portalProxy(account string, actor string, subpath string) { - if subpath == "" { -
- } else { -
- } +templ portalProxy(account string, portal string, subpath string) { +
+
+ +
+
} templ loggedOutPage() { diff --git a/pkg/hz/controller.go b/pkg/hz/controller.go index 5f27c2f..59ffa84 100644 --- a/pkg/hz/controller.go +++ b/pkg/hz/controller.go @@ -288,7 +288,6 @@ func (c *Controller) startReconciler( return fmt.Errorf("create for consumer: %w", err) } cc, err := con.Consume(func(msg jetstream.Msg) { - slog.Info("consumer", "subject", msg.Subject()) kvop := opFromMsg(msg) if kvop == jetstream.KeyValueDelete { // If the operation is a KV delete, then the value has been @@ -419,7 +418,6 @@ func (c *Controller) handleControlLoop( msg jetstream.Msg, ttl time.Duration, ) { - slog.Info("control loop", "key", key) // Check that the message is the last message for the subject. // If not, we don't care about it and want to avoid acquiring the lock. isLast, err := isLastMsg(ctx, kv, msg) @@ -441,7 +439,7 @@ func (c *Controller) handleControlLoop( return } // Get the object key from the nats subject / kv key. - objKey, err := objectKeyFromKey(key) + objKey, err := ObjectKeyFromString(key) if err != nil { slog.Error("getting object key from key", "key", key, "error", err) _ = msg.NakWithDelay(time.Second) @@ -496,6 +494,7 @@ func (c *Controller) handleControlLoop( defer cancel() reconcileResult, reconcileErr = reconciler.Reconcile(ctx, req) } + slog.Info("reconciling object", "key", key) go reconcile() // Setup an auto-ticker for the message, which keeps the message alive and @@ -527,8 +526,6 @@ func (c *Controller) handleControlLoop( } slog.Error( "reconcile", - "subject", - msg.Subject(), "key", req.Key, "backoff", diff --git a/pkg/hz/cue.go b/pkg/hz/cue.go index c715db9..4e2e496 100644 --- a/pkg/hz/cue.go +++ b/pkg/hz/cue.go @@ -12,6 +12,7 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/format" + "github.com/google/uuid" "github.com/verifa/horizon/pkg/internal/managedfields" ) @@ -155,7 +156,7 @@ func cueEncodeField( // Handle special types. iVal := reflect.New(fieldType).Elem().Interface() switch iVal.(type) { - case time.Time: + case Time, time.Time: // This was the best attempt at getting formatting for time, but it // involves importing stuff and complicated things a lot right now. // importTime := ast.NewImport(nil, "time") @@ -168,6 +169,8 @@ func cueEncodeField( case json.RawMessage: // Use lattice type for raw message. return cCtx.BuildExpr(ast.NewIdent("_")), nil + case uuid.UUID: + return cCtx.BuildExpr(ast.NewIdent("string")), nil case managedfields.FieldsV1: // Use lattice type for fieldsv1 because it is recursive. // Would be nice to "solve" recursion in the future... diff --git a/pkg/hz/cue_test.go b/pkg/hz/cue_test.go index 0b72fd1..458b9a1 100644 --- a/pkg/hz/cue_test.go +++ b/pkg/hz/cue_test.go @@ -89,6 +89,7 @@ func TestCueDefinition(t *testing.T) { account: string name: string }] + deletionTimestamp?: string managedFields?: [...{ manager: =~"^[a-zA-Z0-9-_]+$" fieldsType: =~"^FieldsV1$" diff --git a/pkg/hz/object.go b/pkg/hz/object.go index 86f54d3..805a07f 100644 --- a/pkg/hz/object.go +++ b/pkg/hz/object.go @@ -97,7 +97,7 @@ func KeyFromObjectStrict(obj ObjectKeyer) (string, error) { return KeyFromObject(obj), nil } -func objectKeyFromKey(key string) (ObjectKey, error) { +func ObjectKeyFromString(key string) (ObjectKey, error) { parts := strings.Split(key, ".") if len(parts) != 5 { return ObjectKey{}, fmt.Errorf("invalid key: %q", key) @@ -185,15 +185,19 @@ type ObjectMeta struct { // Revision is the revision number of the object. Revision *uint64 `json:"revision,omitempty" cue:",opt"` - OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" cue:",opt"` - DeletionTimestamp *Time `json:"deletionTimestamp,omitempty" cue:"-"` + OwnerReferences OwnerReferences `json:"ownerReferences,omitempty" cue:",opt"` + DeletionTimestamp *Time `json:"deletionTimestamp,omitempty" cue:",opt"` ManagedFields managedfields.ManagedFields `json:"managedFields,omitempty" cue:",opt"` // Finalizers are a way for controllers to prevent garbage collection of // objects. The GC will not delete an object unless it has no finalizers. // Hence, it is the responsibility of the controller to remove the // finalizers once the object has been marked for deletion (by setting the // deletionTimestamp). - Finalizers []string `json:"finalizers,omitempty" cue:",opt"` + // + // Use type alias to "correctly" marshal to json. + // A nil Finalizers is omitted from JSON. + // A non-nil Finalizers is marshalled as an empty array if it is empty. + Finalizers *Finalizers `json:"finalizers,omitempty" cue:",opt"` } func (o ObjectMeta) ObjectName() string { @@ -265,8 +269,8 @@ func (t TypeMeta) ObjectVersion() string { return parts[1] } -func OwnerReferenceFromObject(object Objecter) *OwnerReference { - return &OwnerReference{ +func OwnerReferenceFromObject(object Objecter) OwnerReference { + return OwnerReference{ Group: object.ObjectGroup(), Version: object.ObjectVersion(), Kind: object.ObjectKind(), @@ -275,6 +279,17 @@ func OwnerReferenceFromObject(object Objecter) *OwnerReference { } } +type OwnerReferences []OwnerReference + +func (o OwnerReferences) IsOwnedBy(obj Objecter) bool { + for _, owner := range o { + if owner.IsOwnedBy(obj) { + return true + } + } + return false +} + var _ ObjectKeyer = (*OwnerReference)(nil) type OwnerReference struct { @@ -327,6 +342,29 @@ func (t *Time) IsPast() bool { return t.Before(time.Now()) } +// Finalizers are a way to prevent garbage collection of objects until a +// controller has finished some cleanup logic. +type Finalizers []string + +func (f *Finalizers) Contains(finalizer string) bool { + if f == nil { + return false + } + for _, s := range *f { + if s == finalizer { + return true + } + } + return false +} + +func (f *Finalizers) String() string { + if f == nil { + return "" + } + return strings.Join(*f, ",") +} + var _ Objecter = (*MetaOnlyObject)(nil) // MetaOnlyObject is an object that has no spec or status. diff --git a/pkg/hz/watcher.go b/pkg/hz/watcher.go index c926f37..d017553 100644 --- a/pkg/hz/watcher.go +++ b/pkg/hz/watcher.go @@ -228,7 +228,7 @@ func (w *Watcher) Start(ctx context.Context, opts ...WatcherOption) error { } } rawKey := keyFromMsgSubject(kv, msg) - key, err := objectKeyFromKey(rawKey) + key, err := ObjectKeyFromString(rawKey) if err != nil { slog.Error( "parsing key from subject", @@ -312,17 +312,17 @@ type EventResult struct { Err error } -type EventOperation int +type EventOperation string const ( // EventOperationPut indicates that the object has been created or updated. - EventOperationPut EventOperation = iota + EventOperationPut EventOperation = "put" // EventOperationDelete indicates that the object has been marked for // deletion by setting the metadata.deletionTimestamp field. // It does not mean that the deleteionTimestamp has been reached yet, // so the deletionTimestamp may be in the future. - EventOperationDelete + EventOperationDelete EventOperation = "delete" // EventOperationPurge indicates that the object no longer exists in the kv // store. - EventOperationPurge + EventOperationPurge EventOperation = "purge" ) diff --git a/pkg/hzctl/config.go b/pkg/hzctl/config.go index 2201e9e..ac794a9 100644 --- a/pkg/hzctl/config.go +++ b/pkg/hzctl/config.go @@ -14,10 +14,9 @@ type Config struct { type Context struct { Name string `json:"name,omitempty"` - URL string `json:"url,omitempty"` - Account *string `json:"account,omitempty"` - Credentials *string `json:"credentials,omitempty"` - Session *string `json:"session,omitempty"` + URL string `json:"url,omitempty"` + Account *string `json:"account,omitempty"` + Session *string `json:"session,omitempty"` } type ValidateOption func(*validateOptions) @@ -46,15 +45,9 @@ func (c *Context) Validate(opts ...ValidateOption) error { if c.URL == "" { errs = errors.Join(errs, fmt.Errorf("context url is required")) } - // if c.Account == nil { - // errs = errors.Join(errs, fmt.Errorf("context account is required")) - // } if vOpts.hasSession && c.Session == nil { errs = errors.Join(errs, fmt.Errorf("context session is required")) } - if vOpts.hasCredentials && c.Credentials == nil { - errs = errors.Join(errs, fmt.Errorf("context credentials is required")) - } return errs } diff --git a/pkg/hzctl/login/login.go b/pkg/hzctl/login/login.go index e78b466..05406f2 100644 --- a/pkg/hzctl/login/login.go +++ b/pkg/hzctl/login/login.go @@ -23,8 +23,7 @@ type LoginRequest struct { } type LoginResponse struct { - Session string - Credentials string + Session string } func Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) { @@ -92,15 +91,9 @@ func (l *loginHandler) handleLogin(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - userConfig, err := postNewUser(l.baseURL, sessionCookie) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } // Write response to channel. resp := LoginResponse{ - Session: sessionCookie.Value, - Credentials: string(userConfig), + Session: sessionCookie.Value, } l.resp <- resp _ = layout( diff --git a/pkg/hzctl/login/login.templ b/pkg/hzctl/login/login.templ index 9cf3992..6f5d26c 100644 --- a/pkg/hzctl/login/login.templ +++ b/pkg/hzctl/login/login.templ @@ -24,6 +24,6 @@ templ pageStatusOK(resp LoginResponse) {

Login Successful!

Session: { resp.Session }
-
{ resp.Credentials }
+

You may close this window now

} diff --git a/pkg/store/create.go b/pkg/store/create.go index 02a70cb..c621c1d 100644 --- a/pkg/store/create.go +++ b/pkg/store/create.go @@ -51,8 +51,27 @@ func (s Store) create( key hz.ObjectKeyer, data []byte, ) error { - _, err := s.kv.Create(ctx, hz.KeyFromObject(key), data) + rawKey, err := hz.KeyFromObjectStrict(key) if err != nil { + return &hz.Error{ + Status: http.StatusBadRequest, + Message: fmt.Sprintf( + "invalid key: %q", + err.Error(), + ), + } + } + data, err = removeReadOnlyFields(data) + if err != nil { + return &hz.Error{ + Status: http.StatusInternalServerError, + Message: fmt.Sprintf( + "removing read-only fields: %s", + err.Error(), + ), + } + } + if _, err := s.kv.Create(ctx, rawKey, data); err != nil { if errors.Is(err, jetstream.ErrKeyExists) { return &hz.Error{ Status: http.StatusConflict, diff --git a/pkg/store/gc.go b/pkg/store/gc.go index 269469c..351c5de 100644 --- a/pkg/store/gc.go +++ b/pkg/store/gc.go @@ -56,12 +56,12 @@ func (gc *GarbageCollector) garbageCollect( if err := json.Unmarshal(event.Data, &obj); err != nil { return hz.Result{}, fmt.Errorf("unmarshal object: %w", err) } - // Double check the object has a deletion timestamp. - if obj.ObjectMeta.DeletionTimestamp == nil { + // If the object has no deletion timestamp, it's not ready to be deleted. + if obj.DeletionTimestamp == nil { return hz.Result{}, nil } - // Check that timestamp has expired... so we don't delete prematurely. - if obj.ObjectMeta.DeletionTimestamp.After(time.Now()) { + // Check the deletion is due. If not, reschedule for when it is. + if !obj.ObjectMeta.DeletionTimestamp.IsPast() { // If the deletion timestamp has not expired yet, requeue the event // to be processed once it has. return hz.Result{ @@ -77,11 +77,9 @@ func (gc *GarbageCollector) garbageCollect( }, nil } if result == DeleteResultFinalizers { - // If the object still has finalizers, requeue the event to be processed - // again later. - return hz.Result{ - RequeueAfter: time.Second * 5, - }, nil + // If the object still has finalizers, ACK and we will try again when + // the object is updated. + return hz.Result{}, nil } return hz.Result{}, nil @@ -91,8 +89,17 @@ func (gc *GarbageCollector) deleteObjectCascading( ctx context.Context, obj hz.MetaOnlyObject, ) (DeleteResult, error) { + slog.Info( + "CASCADING DELETE", + "key", + hz.KeyFromObject(obj), + // "revision", + // *obj.ObjectMeta.Revision, + "finalizers", + obj.Finalizers, + ) // If the object has finalizers, it's not ready to be deleted. - if len(obj.ObjectMeta.Finalizers) > 0 { + if obj.ObjectMeta.Finalizers != nil && len(*obj.ObjectMeta.Finalizers) > 0 { return DeleteResultFinalizers, nil } // Check any child objects and delete those first. diff --git a/pkg/store/store.go b/pkg/store/store.go index 876df7c..5239a74 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -13,6 +13,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "github.com/tidwall/sjson" "github.com/verifa/horizon/pkg/auth" "github.com/verifa/horizon/pkg/hz" ) @@ -441,3 +442,7 @@ func (s Store) handleInternalMsg(ctx context.Context, msg *nats.Msg) { _ = hz.RespondOK(msg, nil) } + +func removeReadOnlyFields(data []byte) ([]byte, error) { + return sjson.DeleteBytes(data, "metadata.revision") +} diff --git a/pkg/store/update.go b/pkg/store/update.go index 8d0b526..6446cd1 100644 --- a/pkg/store/update.go +++ b/pkg/store/update.go @@ -43,6 +43,16 @@ func (s Store) update( ), } } + data, err = removeReadOnlyFields(data) + if err != nil { + return &hz.Error{ + Status: http.StatusInternalServerError, + Message: fmt.Sprintf( + "removing read-only fields: %s", + err.Error(), + ), + } + } if _, err := s.kv.Update(ctx, rawKey, data, revision); err != nil { if isErrWrongLastSequence(err) { return hz.ErrIncorrectRevision