// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package demo import ( "context" "fmt" "math/rand" "github.com/oklog/ulid/v2" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller/dependency" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/proto-public/pbresource" pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" ) const statusKeyArtistController = "consul.io/artist-controller" // RegisterControllers registers controllers for the demo types. Should only be // called in dev mode. func RegisterControllers(mgr *controller.Manager) { mgr.Register(artistController()) } func artistController() *controller.Controller { return controller.NewController("artists", TypeV2Artist). WithWatch(TypeV2Album, dependency.MapOwner). WithReconciler(&artistReconciler{}) } type artistReconciler struct{} func (r *artistReconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { rsp, err := rt.Client.Read(ctx, &pbresource.ReadRequest{Id: req.ID}) switch { case status.Code(err) == codes.NotFound: return nil case err != nil: return err } res := rsp.Resource var artist pbdemov2.Artist if err := res.Data.UnmarshalTo(&artist); err != nil { return err } conditions := []*pbresource.Condition{ { Type: "Accepted", State: pbresource.Condition_STATE_TRUE, Reason: "Accepted", Message: fmt.Sprintf("Artist '%s' accepted", artist.Name), }, } numAlbums := 3 if artist.Genre == pbdemov2.Genre_GENRE_BLUES { numAlbums = 10 } desiredAlbums, err := generateV2AlbumsDeterministic(res.Id, numAlbums) if err != nil { return err } actualAlbums, err := rt.Client.List(ctx, &pbresource.ListRequest{ Type: TypeV2Album, Tenancy: res.Id.Tenancy, NamePrefix: fmt.Sprintf("%s-", res.Id.Name), }) if err != nil { return err } writes, deletions, err := diffAlbums(desiredAlbums, actualAlbums.Resources) if err != nil { return err } for _, w := range writes { if _, err := rt.Client.Write(ctx, &pbresource.WriteRequest{Resource: w}); err != nil { return err } } for _, d := range deletions { if _, err := rt.Client.Delete(ctx, &pbresource.DeleteRequest{Id: d}); err != nil { return err } } for _, want := range desiredAlbums { var album pbdemov2.Album if err := want.Data.UnmarshalTo(&album); err != nil { return err } conditions = append(conditions, &pbresource.Condition{ Type: "AlbumCreated", State: pbresource.Condition_STATE_TRUE, Reason: "AlbumCreated", Message: fmt.Sprintf("Album '%s' created for artist '%s'", album.Title, artist.Name), Resource: resource.Reference(want.Id, ""), }) } newStatus := &pbresource.Status{ ObservedGeneration: res.Generation, Conditions: conditions, } if resource.EqualStatus(res.Status[statusKeyArtistController], newStatus, false) { return nil } _, err = rt.Client.WriteStatus(ctx, &pbresource.WriteStatusRequest{ Id: res.Id, Key: statusKeyArtistController, Status: newStatus, }) return err } func diffAlbums(want, have []*pbresource.Resource) ([]*pbresource.Resource, []*pbresource.ID, error) { haveMap := make(map[string]*pbresource.Resource, len(have)) for _, r := range have { haveMap[r.Id.Name] = r } wantMap := make(map[string]struct{}, len(want)) for _, r := range want { wantMap[r.Id.Name] = struct{}{} } writes := make([]*pbresource.Resource, 0) for _, w := range want { h, ok := haveMap[w.Id.Name] if ok { var wd, hd pbdemov2.Album if err := w.Data.UnmarshalTo(&wd); err != nil { return nil, nil, err } if err := h.Data.UnmarshalTo(&hd); err != nil { return nil, nil, err } if proto.Equal(&wd, &hd) { continue } } writes = append(writes, w) } deletions := make([]*pbresource.ID, 0) for _, h := range have { if _, ok := wantMap[h.Id.Name]; ok { continue } deletions = append(deletions, h.Id) } return writes, deletions, nil } func generateV2AlbumsDeterministic(artistID *pbresource.ID, count int) ([]*pbresource.Resource, error) { uid, err := ulid.Parse(artistID.Uid) if err != nil { return nil, fmt.Errorf("failed to parse Uid: %w", err) } rand := rand.New(rand.NewSource(int64(uid.Time()))) albums := make([]*pbresource.Resource, count) for i := 0; i < count; i++ { album, err := generateV2Album(artistID, rand) if err != nil { return nil, err } // Add suffix to avoid collisions. album.Id.Name = fmt.Sprintf("%s-%d", album.Id.Name, i) albums[i] = album } return albums, nil }