Skip to content

Commit

Permalink
SYSENG-1357: generic client default options
Browse files Browse the repository at this point in the history
  • Loading branch information
Mario Schäfer committed Apr 24, 2024
1 parent 110c34a commit 9ead8f6
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 29 deletions.
45 changes: 44 additions & 1 deletion pkg/api/api_implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ type defaultAPI struct {
client client.Client
logger *logr.Logger

clientOptions []client.Option
clientOptions []client.Option
requestOptions []types.Option
}

// NewAPIOption is the type for giving options to the NewAPI function.
Expand All @@ -43,6 +44,13 @@ func WithClientOptions(o ...client.Option) NewAPIOption {
}
}

// WithRequestOptions configures global request options
func WithRequestOptions(opts ...types.Option) NewAPIOption {
return func(a *defaultAPI) {
a.requestOptions = opts
}
}

// WithLogger configures the API to use the given logger. It is recommended to pass a named logger.
// If you don't pass an existing client, the logger you give here will given to the client (with
// added name "client").
Expand Down Expand Up @@ -79,6 +87,13 @@ func NewAPI(opts ...NewAPIOption) (API, error) {
// Get the identified object from the engine.
func (a defaultAPI) Get(ctx context.Context, o types.IdentifiedObject, opts ...types.GetOption) error {
options := types.GetOptions{}
for _, requestOpt := range a.requestOptions {
if getOpt, ok := requestOpt.(types.GetOption); ok {
getOpt.ApplyToGet(&options)

Check failure on line 92 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `getOpt.ApplyToGet` is not checked (errcheck)

Check failure on line 92 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `getOpt.ApplyToGet` is not checked (errcheck)
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
anyOpt.ApplyToAny(&options)

Check failure on line 94 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)

Check failure on line 94 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)
}
}
for _, opt := range opts {
opt.ApplyToGet(&options)

Check failure on line 98 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `opt.ApplyToGet` is not checked (errcheck)

Check failure on line 98 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `opt.ApplyToGet` is not checked (errcheck)
}
Expand All @@ -89,6 +104,13 @@ func (a defaultAPI) Get(ctx context.Context, o types.IdentifiedObject, opts ...t
// Create the given object on the engine.
func (a defaultAPI) Create(ctx context.Context, o types.Object, opts ...types.CreateOption) error {
options := types.CreateOptions{}
for _, requestOpt := range a.requestOptions {
if createOpt, ok := requestOpt.(types.CreateOption); ok {
createOpt.ApplyToCreate(&options)

Check failure on line 109 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `createOpt.ApplyToCreate` is not checked (errcheck)

Check failure on line 109 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `createOpt.ApplyToCreate` is not checked (errcheck)
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
anyOpt.ApplyToAny(&options)

Check failure on line 111 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)

Check failure on line 111 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)
}
}
for _, opt := range opts {
opt.ApplyToCreate(&options)

Check failure on line 115 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `opt.ApplyToCreate` is not checked (errcheck)

Check failure on line 115 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `opt.ApplyToCreate` is not checked (errcheck)
}
Expand All @@ -115,6 +137,13 @@ func (a defaultAPI) handlePostCreateOptions(ctx context.Context, o types.Identif
// Update the object on the engine.
func (a defaultAPI) Update(ctx context.Context, o types.IdentifiedObject, opts ...types.UpdateOption) error {
options := types.UpdateOptions{}
for _, requestOpt := range a.requestOptions {
if updateOpt, ok := requestOpt.(types.UpdateOption); ok {
updateOpt.ApplyToUpdate(&options)

Check failure on line 142 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `updateOpt.ApplyToUpdate` is not checked (errcheck)

Check failure on line 142 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `updateOpt.ApplyToUpdate` is not checked (errcheck)
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
anyOpt.ApplyToAny(&options)

Check failure on line 144 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)

Check failure on line 144 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `anyOpt.ApplyToAny` is not checked (errcheck)
}
}
for _, opt := range opts {
opt.ApplyToUpdate(&options)

Check failure on line 148 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `opt.ApplyToUpdate` is not checked (errcheck)

Check failure on line 148 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `opt.ApplyToUpdate` is not checked (errcheck)
}
Expand All @@ -125,6 +154,13 @@ func (a defaultAPI) Update(ctx context.Context, o types.IdentifiedObject, opts .
// Destroy the identified object.
func (a defaultAPI) Destroy(ctx context.Context, o types.IdentifiedObject, opts ...types.DestroyOption) error {
options := types.DestroyOptions{}
for _, requestOpt := range a.requestOptions {
if destroyOpt, ok := requestOpt.(types.DestroyOption); ok {
destroyOpt.ApplyToDestroy(&options)

Check failure on line 159 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with target Go

Error return value of `destroyOpt.ApplyToDestroy` is not checked (errcheck)

Check failure on line 159 in pkg/api/api_implementation.go

View workflow job for this annotation

GitHub Actions / Static checks with latest Go

Error return value of `destroyOpt.ApplyToDestroy` is not checked (errcheck)
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
anyOpt.ApplyToAny(&options)
}
}
for _, opt := range opts {
opt.ApplyToDestroy(&options)
}
Expand All @@ -135,6 +171,13 @@ func (a defaultAPI) Destroy(ctx context.Context, o types.IdentifiedObject, opts
// List objects matching the info given in the object.
func (a defaultAPI) List(ctx context.Context, o types.FilterObject, opts ...types.ListOption) error {
options := types.ListOptions{}
for _, requestOpt := range a.requestOptions {
if listOpt, ok := requestOpt.(types.ListOption); ok {
listOpt.ApplyToList(&options)
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
anyOpt.ApplyToAny(&options)
}
}
for _, opt := range opts {
opt.ApplyToList(&options)
}
Expand Down
60 changes: 50 additions & 10 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,24 @@ var _ = Describe("getResponseType function", func() {

type apiTestAnyopOption string

func (o apiTestAnyopOption) ApplyToGet(opts *types.GetOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToGet(opts *types.GetOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToList(opts *types.ListOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToList(opts *types.ListOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToCreate(opts *types.CreateOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToCreate(opts *types.CreateOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToUpdate(opts *types.UpdateOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToUpdate(opts *types.UpdateOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToDestroy(opts *types.DestroyOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToDestroy(opts *types.DestroyOptions) error {
return opts.Set("api_test_option", o, false)
}

type apiTestObject struct {
Expand Down Expand Up @@ -1025,6 +1025,46 @@ var _ = Describe("using an API object", func() {
err = api.Destroy(ctx, &o, opt)
Expect(err).NotTo(HaveOccurred())
})

It("consumes the globally set options for all operations", func() {
opt := apiTestAnyopOption("hello world")
ctx := context.WithValue(context.TODO(), errAPITest, opt)

server.AppendHandlers(
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, []map[string]string{{"value": "option-check"}}),
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, map[string]string{}),
)

api, err := NewAPI(
WithLogger(logger),
WithClientOptions(
client.BaseURL(server.URL()),
client.IgnoreMissingToken(),
),
WithRequestOptions(opt),
)
Expect(err).NotTo(HaveOccurred())

o := apiTestObject{"option-check"}

err = api.Create(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Get(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.List(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Update(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Destroy(ctx, &o)
Expect(err).NotTo(HaveOccurred())
})
})

const contextTestObjectBaseurl = "/v1/context_test_object"
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ var (

// ErrContextRequired is returned when a nil context was passed as argument.
ErrContextRequired = errors.New("no context given")

// ErrEnvironmentSegmentNotTypeString is returned when the environment segment is not of type string
ErrEnvironmentSegmentNotTypeString = errors.New("environment segment is not of type string")
)

// EngineError is the base type for all errors returned by the engine.
Expand Down
12 changes: 8 additions & 4 deletions pkg/api/internal/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ type PagedOption struct {
}

// ApplyToList applies the Paged option to all the ListOptions.
func (p PagedOption) ApplyToList(o *types.ListOptions) {
func (p PagedOption) ApplyToList(o *types.ListOptions) error {
o.Paged = true
o.Page = p.Page
o.EntriesPerPage = p.Limit
o.PageInfo = p.Info
return nil
}

// ObjectChannelOption configures the List operation to return the objects via the given channel.
Expand All @@ -30,23 +31,26 @@ type ObjectChannelOption struct {
}

// ApplyToList applies the AsObjectChannel option to all the ListOptions.
func (aoc ObjectChannelOption) ApplyToList(o *types.ListOptions) {
func (aoc ObjectChannelOption) ApplyToList(o *types.ListOptions) error {
o.ObjectChannel = aoc.Channel
return nil
}

// FullObjectsOption configures if the List operation shall make a Get operation for each object before
// returning it to the caller.
type FullObjectsOption bool

// ApplyToList applies the FullObjectsOption option to all the ListOptions.
func (foo FullObjectsOption) ApplyToList(o *types.ListOptions) {
func (foo FullObjectsOption) ApplyToList(o *types.ListOptions) error {
o.FullObjects = bool(foo)
return nil
}

// AutoTagOption configures the Create operation to automatically tag objects after creation
type AutoTagOption []string

// ApplyToCreate applies the AutoTagOption to the ListOptions
func (ato AutoTagOption) ApplyToCreate(o *types.CreateOptions) {
func (ato AutoTagOption) ApplyToCreate(o *types.CreateOptions) error {
o.AutoTags = ato
return nil
}
18 changes: 18 additions & 0 deletions pkg/api/object_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,21 @@ func Example_implementObject() {
// Retrieved object with mode 'tcp' named 'hello TCP 1'
// Retrieved object with mode 'tcp' named 'hello TCP 2'
}

func ExampleWithRequestOptions() {
api, err := NewAPI(
WithRequestOptions(
// automatically assign tags to newly created resources
AutoTag("foo", "bar"),
),
)

if err != nil {
panic(fmt.Errorf("Error creating API instance: %v\n", err))
}

// create resource and automatically apply 'foo' & 'bar' tags
if err := api.Create(context.TODO(), &ExampleObject{Name: "foo"}); err != nil {
panic(err)
}
}
33 changes: 33 additions & 0 deletions pkg/api/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api

import (
"context"
"errors"
"fmt"

"go.anx.io/go-anxcloud/pkg/api/internal"
"go.anx.io/go-anxcloud/pkg/api/types"
)
Expand Down Expand Up @@ -34,3 +38,32 @@ func FullObjects(fullObjects bool) ListOption {
func AutoTag(tags ...string) CreateOption {
return internal.AutoTagOption(tags)
}

// EnvironmentOption can be used to configure an alternative environment path
// segment for a given API group
type EnvironmentOption struct {
APIGroup string
EnvPathSegment string
Override bool
}

// ApplyToAny applies the EnvironmentOption to any request type
func (o EnvironmentOption) ApplyToAny(opts types.Options) error {
return opts.Set(fmt.Sprintf("environment/%s", o.APIGroup), o.EnvPathSegment, o.Override)
}

// GetEnvironmentPathSegment retrieves the environment path segment of a given API group
// or the provided defaultValue if no environment override is set
func GetEnvironmentPathSegment(ctx context.Context, apiGroup, defaultValue string) (string, error) {
if options, err := types.OptionsFromContext(ctx); err != nil {
return "", err
} else if env, err := options.Get(fmt.Sprintf("environment/%s", apiGroup)); errors.Is(err, types.ErrKeyNotSet) {
return defaultValue, nil
} else if err != nil {
return "", err
} else if envString, ok := env.(string); !ok {
return "", ErrEnvironmentSegmentNotTypeString
} else {
return envString, nil
}
}
16 changes: 11 additions & 5 deletions pkg/api/types/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,39 @@ type Options interface {
// GetOption is the interface options have to implement to be usable with Get operation.
type GetOption interface {
// Apply this option to the set of all options
ApplyToGet(*GetOptions)
ApplyToGet(*GetOptions) error
}

// ListOption is the interface options have to implement to be usable with List operation.
type ListOption interface {
// Apply this option to the set of all options
ApplyToList(*ListOptions)
ApplyToList(*ListOptions) error
}

// CreateOption is the interface options have to implement to be usable with Create operation.
type CreateOption interface {
// Apply this option to the set of all options
ApplyToCreate(*CreateOptions)
ApplyToCreate(*CreateOptions) error
}

// UpdateOption is the interface options have to implement to be usable with Update operation.
type UpdateOption interface {
// Apply this option to the set of all options
ApplyToUpdate(*UpdateOptions)
ApplyToUpdate(*UpdateOptions) error
}

// DestroyOption is the interface options have to implement to be usable with Destroy operation.
type DestroyOption interface {
// Apply this option to the set of all options
ApplyToDestroy(*DestroyOptions)
ApplyToDestroy(*DestroyOptions) error
}

type AnyOption interface {
ApplyToAny(Options) error
}

type Option interface{}

func (o commonOptions) Get(key string) (interface{}, error) {
if o.additional != nil {
if v, ok := o.additional[key]; ok {
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/kubernetes/v1/cluster_genclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var ErrManagedPrefixSet = errors.New("managed prefixes cannot be set on create")

// EndpointURL returns the common URL for operations on Cluster resource
func (c *Cluster) EndpointURL(ctx context.Context) (*url.URL, error) {
return endpointURL(ctx, c, "/api/kubernetes/v1/cluster.json")
return endpointURL(ctx, c, "cluster")
}

// explicitlyFalse returns true if the value of the provided
Expand Down
10 changes: 8 additions & 2 deletions pkg/apis/kubernetes/v1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package v1

import (
"context"
"fmt"
"net/url"

"go.anx.io/go-anxcloud/pkg/api"
"go.anx.io/go-anxcloud/pkg/api/types"
"go.anx.io/go-anxcloud/pkg/utils/object/filter"
)

func endpointURL(ctx context.Context, o types.Object, apiPath string) (*url.URL, error) {
func endpointURL(ctx context.Context, o types.Object, resourcePathName string) (*url.URL, error) {
op, err := types.OperationFromContext(ctx)
if err != nil {
return nil, err
Expand All @@ -19,8 +20,13 @@ func endpointURL(ctx context.Context, o types.Object, apiPath string) (*url.URL,
return nil, api.ErrOperationNotSupported
}

env, err := api.GetEnvironmentPathSegment(ctx, "kubernetes/v1", "kubernetes")
if err != nil {
return nil, fmt.Errorf("get environment: %w", err)
}

// we can ignore the error since the URL is hard-coded known as valid
u, _ := url.Parse(apiPath)
u, _ := url.Parse(fmt.Sprintf("/api/%s/v1/%s.json", env, resourcePathName))

if op == types.OperationList {
helper, err := filter.NewHelper(o)
Expand Down
Loading

0 comments on commit 9ead8f6

Please sign in to comment.