From c9f1c53693b4f83769984b49338b228cb8a47655 Mon Sep 17 00:00:00 2001 From: Hugo <10965479+hugoghx@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:34:31 +0000 Subject: [PATCH] feat(api): Policies --- api/policies/option.gen.go | 188 ++++++++ api/policies/policy.gen.go | 451 ++++++++++++++++++ api/policies/storage_policy_attributes.gen.go | 41 ++ internal/api/genapi/input.go | 58 +++ 4 files changed, 738 insertions(+) create mode 100644 api/policies/option.gen.go create mode 100644 api/policies/policy.gen.go create mode 100644 api/policies/storage_policy_attributes.gen.go diff --git a/api/policies/option.gen.go b/api/policies/option.gen.go new file mode 100644 index 0000000000..350af7e719 --- /dev/null +++ b/api/policies/option.gen.go @@ -0,0 +1,188 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package policies + +import ( + "strconv" + "strings" + + "github.com/hashicorp/boundary/api" +) + +// Option is a func that sets optional attributes for a call. This does not need +// to be used directly, but instead option arguments are built from the +// functions in this package. WithX options set a value to that given in the +// argument; DefaultX options indicate that the value should be set to its +// default. When an API call is made options are processed in ther order they +// appear in the function call, so for a given argument X, a succession of WithX +// or DefaultX calls will result in the last call taking effect. +type Option func(*options) + +type options struct { + postMap map[string]interface{} + queryMap map[string]string + withAutomaticVersioning bool + withSkipCurlOutput bool + withFilter string + withListToken string + withRecursive bool +} + +func getDefaultOptions() options { + return options{ + postMap: make(map[string]interface{}), + queryMap: make(map[string]string), + } +} + +func getOpts(opt ...Option) (options, []api.Option) { + opts := getDefaultOptions() + for _, o := range opt { + if o != nil { + o(&opts) + } + } + var apiOpts []api.Option + if opts.withSkipCurlOutput { + apiOpts = append(apiOpts, api.WithSkipCurlOutput(true)) + } + if opts.withFilter != "" { + opts.queryMap["filter"] = opts.withFilter + } + if opts.withListToken != "" { + opts.queryMap["list_token"] = opts.withListToken + } + if opts.withRecursive { + opts.queryMap["recursive"] = strconv.FormatBool(opts.withRecursive) + } + return opts, apiOpts +} + +// If set, and if the version is zero during an update, the API will perform a +// fetch to get the current version of the resource and populate it during the +// update call. This is convenient but opens up the possibility for subtle +// order-of-modification issues, so use carefully. +func WithAutomaticVersioning(enable bool) Option { + return func(o *options) { + o.withAutomaticVersioning = enable + } +} + +// WithSkipCurlOutput tells the API to not use the current call for cURL output. +// Useful for when we need to look up versions. +func WithSkipCurlOutput(skip bool) Option { + return func(o *options) { + o.withSkipCurlOutput = true + } +} + +// WithListToken tells the API to use the provided list token +// for listing operations on this resource. +func WithListToken(listToken string) Option { + return func(o *options) { + o.withListToken = listToken + } +} + +// WithFilter tells the API to filter the items returned using the provided +// filter term. The filter should be in a format supported by +// hashicorp/go-bexpr. +func WithFilter(filter string) Option { + return func(o *options) { + o.withFilter = strings.TrimSpace(filter) + } +} + +// WithRecursive tells the API to use recursion for listing operations on this +// resource +func WithRecursive(recurse bool) Option { + return func(o *options) { + o.withRecursive = true + } +} + +func WithAttributes(inAttributes map[string]interface{}) Option { + return func(o *options) { + o.postMap["attributes"] = inAttributes + } +} + +func DefaultAttributes() Option { + return func(o *options) { + o.postMap["attributes"] = nil + } +} + +func WithStoragePolicyDeleteAfter(inDeleteAfter map[string]any) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["delete_after"] = inDeleteAfter + o.postMap["attributes"] = val + } +} + +func DefaultStoragePolicyDeleteAfter() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["delete_after"] = nil + o.postMap["attributes"] = val + } +} + +func WithDescription(inDescription string) Option { + return func(o *options) { + o.postMap["description"] = inDescription + } +} + +func DefaultDescription() Option { + return func(o *options) { + o.postMap["description"] = nil + } +} + +func WithName(inName string) Option { + return func(o *options) { + o.postMap["name"] = inName + } +} + +func DefaultName() Option { + return func(o *options) { + o.postMap["name"] = nil + } +} + +func WithStoragePolicyRetainFor(inRetainFor map[string]any) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["retain_for"] = inRetainFor + o.postMap["attributes"] = val + } +} + +func DefaultStoragePolicyRetainFor() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["retain_for"] = nil + o.postMap["attributes"] = val + } +} diff --git a/api/policies/policy.gen.go b/api/policies/policy.gen.go new file mode 100644 index 0000000000..869e2a8a71 --- /dev/null +++ b/api/policies/policy.gen.go @@ -0,0 +1,451 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package policies + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "slices" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" +) + +type Policy struct { + Id string `json:"id,omitempty"` + ScopeId string `json:"scope_id,omitempty"` + Scope *scopes.ScopeInfo `json:"scope,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` + UpdatedTime time.Time `json:"updated_time,omitempty"` + Type string `json:"type,omitempty"` + Version uint32 `json:"version,omitempty"` + Attributes map[string]interface{} `json:"attributes,omitempty"` + AuthorizedActions []string `json:"authorized_actions,omitempty"` + + response *api.Response +} + +type PolicyReadResult struct { + Item *Policy + response *api.Response +} + +func (n PolicyReadResult) GetItem() *Policy { + return n.Item +} + +func (n PolicyReadResult) GetResponse() *api.Response { + return n.response +} + +type PolicyCreateResult = PolicyReadResult +type PolicyUpdateResult = PolicyReadResult + +type PolicyDeleteResult struct { + response *api.Response +} + +// GetItem will always be nil for PolicyDeleteResult +func (n PolicyDeleteResult) GetItem() interface{} { + return nil +} + +func (n PolicyDeleteResult) GetResponse() *api.Response { + return n.response +} + +type PolicyListResult struct { + Items []*Policy `json:"items,omitempty"` + EstItemCount uint `json:"est_item_count,omitempty"` + RemovedIds []string `json:"removed_ids,omitempty"` + ListToken string `json:"list_token,omitempty"` + ResponseType string `json:"response_type,omitempty"` + response *api.Response +} + +func (n PolicyListResult) GetItems() []*Policy { + return n.Items +} + +func (n PolicyListResult) GetEstItemCount() uint { + return n.EstItemCount +} + +func (n PolicyListResult) GetRemovedIds() []string { + return n.RemovedIds +} + +func (n PolicyListResult) GetListToken() string { + return n.ListToken +} + +func (n PolicyListResult) GetResponseType() string { + return n.ResponseType +} + +func (n PolicyListResult) GetResponse() *api.Response { + return n.response +} + +// Client is a client for this collection +type Client struct { + client *api.Client +} + +// Creates a new client for this collection. The submitted API client is cloned; +// modifications to it after generating this client will not have effect. If you +// need to make changes to the underlying API client, use ApiClient() to access +// it. +func NewClient(c *api.Client) *Client { + return &Client{client: c.Clone()} +} + +// ApiClient returns the underlying API client +func (c *Client) ApiClient() *api.Client { + return c.client +} + +func (c *Client) Create(ctx context.Context, resourceType string, scopeId string, opt ...Option) (*PolicyCreateResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into Create request") + } + + opts, apiOpts := getOpts(opt...) + + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + if resourceType == "" { + return nil, fmt.Errorf("empty resourceType value passed into Create request") + } else { + opts.postMap["type"] = resourceType + } + + opts.postMap["scope_id"] = scopeId + + req, err := c.client.NewRequest(ctx, "POST", "policies", opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Create request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during Create call: %w", err) + } + + target := new(PolicyCreateResult) + target.Item = new(Policy) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding Create response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Read(ctx context.Context, id string, opt ...Option) (*PolicyReadResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Read request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("policies/%s", url.PathEscape(id)), nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Read request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error performing client request during Read call: %w", err) + } + + target := new(PolicyReadResult) + target.Item = new(Policy) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding Read response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Update(ctx context.Context, id string, version uint32, opt ...Option) (*PolicyUpdateResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Update request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + if version == 0 { + if !opts.withAutomaticVersioning { + return nil, errors.New("zero version number passed into Update request and automatic versioning not specified") + } + existingTarget, existingErr := c.Read(ctx, id, append([]Option{WithSkipCurlOutput(true)}, opt...)...) + if existingErr != nil { + if api.AsServerError(existingErr) != nil { + return nil, fmt.Errorf("error from controller when performing initial check-and-set read: %w", existingErr) + } + return nil, fmt.Errorf("error performing initial check-and-set read: %w", existingErr) + } + if existingTarget == nil { + return nil, errors.New("nil resource response found when performing initial check-and-set read") + } + if existingTarget.Item == nil { + return nil, errors.New("nil resource found when performing initial check-and-set read") + } + version = existingTarget.Item.Version + } + + opts.postMap["version"] = version + + req, err := c.client.NewRequest(ctx, "PATCH", fmt.Sprintf("policies/%s", url.PathEscape(id)), opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Update request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during Update call: %w", err) + } + + target := new(PolicyUpdateResult) + target.Item = new(Policy) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding Update response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Delete(ctx context.Context, id string, opt ...Option) (*PolicyDeleteResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Delete request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("policies/%s", url.PathEscape(id)), nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Delete request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during Delete call: %w", err) + } + + apiErr, err := resp.Decode(nil) + if err != nil { + return nil, fmt.Errorf("error decoding Delete response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + + target := &PolicyDeleteResult{ + response: resp, + } + return target, nil +} + +func (c *Client) List(ctx context.Context, scopeId string, opt ...Option) (*PolicyListResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into List request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + opts.queryMap["scope_id"] = scopeId + + req, err := c.client.NewRequest(ctx, "GET", "policies", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating List request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during List call: %w", err) + } + + target := new(PolicyListResult) + apiErr, err := resp.Decode(target) + if err != nil { + return nil, fmt.Errorf("error decoding List response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + if target.ResponseType == "complete" || target.ResponseType == "" { + return target, nil + } + // If there are more results, automatically fetch the rest of the results. + // idToIndex keeps a map from the ID of an item to its index in target.Items. + // This is used to update updated items in-place and remove deleted items + // from the result after pagination is done. + idToIndex := map[string]int{} + for i, item := range target.Items { + idToIndex[item.Id] = i + } + // Removed IDs in the response may contain duplicates, + // maintain a set to avoid returning duplicates to the user. + removedIds := map[string]struct{}{} + for { + req, err := c.client.NewRequest(ctx, "GET", "policies", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating List request: %w", err) + } + + opts.queryMap["list_token"] = target.ListToken + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during List call: %w", err) + } + + page := new(PolicyListResult) + apiErr, err := resp.Decode(page) + if err != nil { + return nil, fmt.Errorf("error decoding List response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + for _, item := range page.Items { + if i, ok := idToIndex[item.Id]; ok { + // Item has already been seen at index i, update in-place + target.Items[i] = item + } else { + target.Items = append(target.Items, item) + idToIndex[item.Id] = len(target.Items) - 1 + } + } + for _, removedId := range page.RemovedIds { + removedIds[removedId] = struct{}{} + } + target.EstItemCount = page.EstItemCount + target.ListToken = page.ListToken + target.ResponseType = page.ResponseType + target.response = resp + if target.ResponseType == "complete" { + break + } + } + for _, removedId := range target.RemovedIds { + if i, ok := idToIndex[removedId]; ok { + // Remove the item at index i without preserving order + // https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order + target.Items[i] = target.Items[len(target.Items)-1] + target.Items = target.Items[:len(target.Items)-1] + // Update the index of the last element + idToIndex[target.Items[i].Id] = i + } + } + for deletedId := range removedIds { + target.RemovedIds = append(target.RemovedIds, deletedId) + } + // Sort to make response deterministic + slices.Sort(target.RemovedIds) + // Since we paginated to the end, we can avoid confusion + // for the user by setting the estimated item count to the + // length of the items slice. If we don't set this here, it + // will equal the value returned in the last response, which is + // often much smaller than the total number returned. + target.EstItemCount = uint(len(target.Items)) + // Sort the results again since in-place updates and deletes + // may have shuffled items. We sort by created time descending + // (most recently created first), same as the API. + slices.SortFunc(target.Items, func(i, j *Policy) int { + return j.CreatedTime.Compare(i.CreatedTime) + }) + // Finally, since we made at least 2 requests to the server to fulfill this + // function call, resp.Body and resp.Map will only contain the most recent response. + // Overwrite them with the true response. + target.response.Body.Reset() + if err := json.NewEncoder(target.response.Body).Encode(target); err != nil { + return nil, fmt.Errorf("error encoding final JSON list response: %w", err) + } + if err := json.Unmarshal(target.response.Body.Bytes(), &target.response.Map); err != nil { + return nil, fmt.Errorf("error encoding final map list response: %w", err) + } + // Note: the HTTP response body is consumed by resp.Decode in the loop, + // so it doesn't need to be updated (it will always be, and has always been, empty). + return target, nil +} diff --git a/api/policies/storage_policy_attributes.gen.go b/api/policies/storage_policy_attributes.gen.go new file mode 100644 index 0000000000..d6dee8bfcc --- /dev/null +++ b/api/policies/storage_policy_attributes.gen.go @@ -0,0 +1,41 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package policies + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type StoragePolicyAttributes struct { + RetainFor map[string]any `json:"retain_for,omitempty"` + DeleteAfter map[string]any `json:"delete_after,omitempty"` +} + +func AttributesMapToStoragePolicyAttributes(in map[string]interface{}) (*StoragePolicyAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out StoragePolicyAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *Policy) GetStoragePolicyAttributes() (*StoragePolicyAttributes, error) { + if pt.Type != "storage" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but policy is of type %s", "storage", pt.Type) + } + return AttributesMapToStoragePolicyAttributes(pt.Attributes) +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 2ffe40dbc2..9b05053df6 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/managedgroups" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/policies" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/roles" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/session_recordings" @@ -753,6 +754,63 @@ var inputStructs = []*structInfo{ recursiveListing: true, }, + // Policy-related resources. + { + inProto: &policies.StoragePolicyAttributes{}, + outFile: "policies/storage_policy_attributes.gen.go", + parentTypeName: "Policy", + subtypeName: "StoragePolicy", + subtype: "storage", + fieldOverrides: []fieldInfo{ + { + Name: "RetainFor", + ProtoName: "retain_for", + FieldType: "map[string]any", + }, + { + Name: "DeleteAfter", + ProtoName: "delete_after", + FieldType: "map[string]any", + }, + }, + templates: []*template.Template{mapstructureConversionTemplate}, + }, + { + inProto: &policies.Policy{}, + outFile: "policies/policy.gen.go", + templates: []*template.Template{ + clientTemplate, + template.Must(template.New("").Funcs( + template.FuncMap{ + "snakeCase": snakeCase, + "funcName": func() string { + return "Create" + }, + "apiAction": func() string { + return "" + }, + "extraRequiredParams": func() []requiredParam { + return []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + } + }, + }, + ).Parse(createTemplateStr)), + readTemplate, + updateTemplate, + deleteTemplate, + listTemplate, + }, + pluralResourceName: "policies", + createResponseTypes: []string{CreateResponseType, ReadResponseType, UpdateResponseType, DeleteResponseType, ListResponseType}, + versionEnabled: true, + recursiveListing: true, + }, + // Host related resources { inProto: &hostcatalogs.HostCatalog{},