package pkger import ( "bytes" "context" "encoding/json" "sort" "time" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/kv" ) type ( entStack struct { ID []byte `json:"id"` OrgID []byte `json:"orgID"` CreatedAt time.Time `json:"createdAt"` Events []entStackEvent `json:"events"` // this embedding is for stacks that were // created before events, this should stay // for some time. entStackEvent } entStackEvent struct { EventType StackEventType `json:"eventType"` Name string `json:"name"` Description string `json:"description"` Sources []string `json:"sources,omitempty"` URLs []string `json:"urls,omitempty"` Resources []entStackResource `json:"resources,omitempty"` UpdatedAt time.Time `json:"updatedAt"` } entStackResource struct { APIVersion string `json:"apiVersion"` ID string `json:"id"` Kind string `json:"kind"` Name string `json:"name"` Associations []entStackAssociation `json:"associations,omitempty"` } entStackAssociation struct { Kind string `json:"kind"` Name string `json:"name"` } ) // StoreKV is a store implementation that uses a kv store backing. type StoreKV struct { kvStore kv.Store indexBase *kv.IndexStore } var _ Store = (*StoreKV)(nil) // NewStoreKV creates a new StoreKV entity. This does not initialize the store. You will // want to init it if you want to have this init donezo at startup. If not it'll lazy // load the buckets as they are used. func NewStoreKV(store kv.Store) *StoreKV { const resource = "stack" storeKV := &StoreKV{ kvStore: store, } storeKV.indexBase = &kv.IndexStore{ Resource: resource, EntStore: storeKV.entStoreBase(resource), IndexStore: storeKV.indexStoreBase(resource), } return storeKV } // CreateStack will create a new stack. If collisions are found will fail. func (s *StoreKV) CreateStack(ctx context.Context, stack Stack) error { return s.put(ctx, stack, kv.PutNew()) } // ListStacks returns a list of stacks. func (s *StoreKV) ListStacks(ctx context.Context, orgID platform.ID, f ListFilter) ([]Stack, error) { if len(f.StackIDs) > 0 && len(f.Names) == 0 { return s.listStacksByID(ctx, orgID, f.StackIDs) } filterFn, err := storeListFilterFn(orgID, f) if err != nil { return nil, err } var stacks []Stack err = s.view(ctx, func(tx kv.Tx) error { return s.indexBase.Find(ctx, tx, kv.FindOpts{ CaptureFn: func(key []byte, decodedVal interface{}) error { stack, err := convertStackEntToStack(decodedVal.(*entStack)) if err != nil { return err } stacks = append(stacks, stack) return nil }, FilterEntFn: func(key []byte, decodedVal interface{}) bool { st := decodedVal.(*entStack) return filterFn(st) }, }) }) if err != nil { return nil, err } return stacks, nil } func storeListFilterFn(orgID platform.ID, f ListFilter) (func(*entStack) bool, error) { orgIDEncoded, err := orgID.Encode() if err != nil { return nil, err } mIDs := make(map[string]bool) for _, id := range f.StackIDs { b, err := id.Encode() if err != nil { return nil, err } mIDs[string(b)] = true } mNames := make(map[string]bool) for _, name := range f.Names { mNames[name] = true } optionalFieldFilterFn := func(ent *entStack) bool { switch { case mIDs[string(ent.ID)]: return true // existing data before stacks are event sourced have // this shape. case len(mNames) > 0 && ent.Name != "": return mNames[ent.Name] case len(mNames) > 0 && len(ent.Events) > 0: sort.Slice(ent.Events, func(i, j int) bool { return ent.Events[i].UpdatedAt.After(ent.Events[j].UpdatedAt) }) return mNames[ent.Events[0].Name] } return true } return func(st *entStack) bool { return bytes.Equal(orgIDEncoded, st.OrgID) && optionalFieldFilterFn(st) }, nil } func (s *StoreKV) listStacksByID(ctx context.Context, orgID platform.ID, stackIDs []platform.ID) ([]Stack, error) { var stacks []Stack for _, id := range stackIDs { st, err := s.ReadStackByID(ctx, id) if errors.ErrorCode(err) == errors.ENotFound { // since the stackIDs are a filter, if it is not found, we just continue // on. If the user wants to verify the existence of a particular stack // then it would be upon them to use the ReadByID call. continue } if err != nil { return nil, err } if orgID != st.OrgID { continue } stacks = append(stacks, st) } return stacks, nil } // ReadStackByID reads a stack by the provided ID. func (s *StoreKV) ReadStackByID(ctx context.Context, id platform.ID) (Stack, error) { var stack Stack err := s.view(ctx, func(tx kv.Tx) error { decodedEnt, err := s.indexBase.FindEnt(ctx, tx, kv.Entity{PK: kv.EncID(id)}) if err != nil { return err } stack, err = convertStackEntToStack(decodedEnt.(*entStack)) return err }) return stack, err } // UpdateStack updates a stack. func (s *StoreKV) UpdateStack(ctx context.Context, stack Stack) error { existing, err := s.ReadStackByID(ctx, stack.ID) if err != nil { return err } if stack.OrgID != existing.OrgID { return &errors.Error{ Code: errors.EUnprocessableEntity, Msg: "org id does not match", } } return s.put(ctx, stack, kv.PutUpdate()) } // DeleteStack deletes a stack by id. func (s *StoreKV) DeleteStack(ctx context.Context, id platform.ID) error { return s.kvStore.Update(ctx, func(tx kv.Tx) error { return s.indexBase.DeleteEnt(ctx, tx, kv.Entity{PK: kv.EncID(id)}) }) } func (s *StoreKV) put(ctx context.Context, stack Stack, opts ...kv.PutOptionFn) error { ent, err := convertStackToEnt(stack) if err != nil { return influxErr(errors.EInvalid, err) } return s.kvStore.Update(ctx, func(tx kv.Tx) error { return s.indexBase.Put(ctx, tx, ent, opts...) }) } func (s *StoreKV) entStoreBase(resource string) *kv.StoreBase { var decodeEntFn kv.DecodeBucketValFn = func(key, val []byte) (keyRepeat []byte, decodedVal interface{}, err error) { var stack entStack return key, &stack, json.Unmarshal(val, &stack) } var decValToEntFn kv.ConvertValToEntFn = func(k []byte, i interface{}) (kv.Entity, error) { s, ok := i.(*entStack) if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil { return kv.Entity{}, err } return kv.Entity{ PK: kv.EncBytes(s.ID), UniqueKey: kv.Encode(kv.EncBytes(s.OrgID), kv.EncBytes(s.ID)), Body: s, }, nil } entityBucket := []byte("v1_pkger_stacks") return kv.NewStoreBase(resource, entityBucket, kv.EncIDKey, kv.EncBodyJSON, decodeEntFn, decValToEntFn) } func (s *StoreKV) indexStoreBase(resource string) *kv.StoreBase { var decValToEntFn kv.ConvertValToEntFn = func(k []byte, v interface{}) (kv.Entity, error) { id, ok := v.(platform.ID) if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil { return kv.Entity{}, err } return kv.Entity{ PK: kv.EncID(id), UniqueKey: kv.EncBytes(k), }, nil } indexBucket := []byte("v1_pkger_stacks_index") return kv.NewStoreBase(resource, indexBucket, kv.EncUniqKey, kv.EncIDKey, kv.DecIndexID, decValToEntFn) } func (s *StoreKV) view(ctx context.Context, fn func(tx kv.Tx) error) error { return s.kvStore.View(ctx, fn) } func convertStackToEnt(stack Stack) (kv.Entity, error) { idBytes, err := stack.ID.Encode() if err != nil { return kv.Entity{}, err } orgIDBytes, err := stack.OrgID.Encode() if err != nil { return kv.Entity{}, err } stEnt := entStack{ ID: idBytes, OrgID: orgIDBytes, CreatedAt: stack.CreatedAt, } for _, ev := range stack.Events { var resources []entStackResource for _, res := range ev.Resources { var associations []entStackAssociation for _, ass := range res.Associations { associations = append(associations, entStackAssociation{ Kind: ass.Kind.String(), Name: ass.MetaName, }) } resources = append(resources, entStackResource{ APIVersion: res.APIVersion, ID: res.ID.String(), Kind: res.Kind.String(), Name: res.MetaName, Associations: associations, }) } stEnt.Events = append(stEnt.Events, entStackEvent{ EventType: ev.EventType, Name: ev.Name, Description: ev.Description, Sources: ev.Sources, URLs: ev.TemplateURLs, Resources: resources, UpdatedAt: ev.UpdatedAt, }) } return kv.Entity{ PK: kv.EncBytes(stEnt.ID), UniqueKey: kv.Encode(kv.EncBytes(stEnt.OrgID), kv.EncBytes(stEnt.ID)), Body: stEnt, }, nil } func convertStackEntToStack(ent *entStack) (Stack, error) { stack := Stack{ CreatedAt: ent.CreatedAt, } if err := stack.ID.Decode(ent.ID); err != nil { return Stack{}, err } if err := stack.OrgID.Decode(ent.OrgID); err != nil { return Stack{}, err } entEvents := ent.Events // ensure backwards compatibility. All existing fields // will be associated with a createEvent, regardless if // they are or not if !ent.UpdatedAt.IsZero() { entEvents = append(entEvents, ent.entStackEvent) } for _, entEv := range entEvents { ev, err := convertEntStackEvent(entEv) if err != nil { return Stack{}, err } stack.Events = append(stack.Events, ev) } return stack, nil } func convertEntStackEvent(ent entStackEvent) (StackEvent, error) { ev := StackEvent{ EventType: ent.EventType, Name: ent.Name, Description: ent.Description, Sources: ent.Sources, TemplateURLs: ent.URLs, UpdatedAt: ent.UpdatedAt, } out, err := convertStackEntResources(ent.Resources) if err != nil { return StackEvent{}, err } ev.Resources = out return ev, nil } func convertStackEntResources(entResources []entStackResource) ([]StackResource, error) { var out []StackResource for _, res := range entResources { stackRes := StackResource{ APIVersion: res.APIVersion, Kind: Kind(res.Kind), MetaName: res.Name, } if err := stackRes.ID.DecodeFromString(res.ID); err != nil { return nil, err } for _, ass := range res.Associations { stackRes.Associations = append(stackRes.Associations, StackResourceAssociation{ Kind: Kind(ass.Kind), MetaName: ass.Name, }) } out = append(out, stackRes) } return out, nil }