api

package
v0.7.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2024 License: Apache-2.0 Imports: 17 Imported by: 10

Documentation

Overview

Package api implements a generic API client for our engine, reducing the amount of duplicated code in this library.

It is heavily inspired by the kubernetes client-go, but also allows handling quirks in the engine API in a graceful way, without making quirky APIs incompatible with the generic code in this package or letting the user of this library handle the quirks.

Example (ImplementObject)
mock := ExampleObjectMockHandler{
	filtered: []ExampleFilterableObject{
		{Name: "hello TCP 1", Mode: "tcp", Identifier: "random identifier 1"},
		{Name: "hello UDP 1", Mode: "udp", Identifier: "random identifier 2"},
		{Name: "hello TCP 2", Mode: "tcp", Identifier: "random identifier 3"},
		{Name: "hello UDP 2", Mode: "udp", Identifier: "random identifier 4"},
	},
}

server := httptest.NewServer(&mock)

api, err := NewAPI(
	WithClientOptions(
		client.IgnoreMissingToken(),
		client.BaseURL(server.URL),
	),
)

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

ctx := context.TODO()

// trying to create an ExampleObject on the API
o := ExampleObject{Name: "hello world"}
if err := api.Create(ctx, &o); err != nil {
	fmt.Printf("Error creating object on API: %v\n", err)
	return
}

fmt.Printf("Object created, identifier '%v'\n", o.Identifier)

// trying to list ExampleFilterableObjects on the API, filtered on mode=tcp
fo := ExampleFilterableObject{Mode: "tcp"}
var fopi types.PageInfo
if err := api.List(ctx, &fo, Paged(1, 1, &fopi)); err != nil {
	fmt.Printf("Error listing objects on API: %v\n", err)
	return
}

var fos []ExampleFilterableObject
for fopi.Next(&fos) {
	for _, fo := range fos {
		fmt.Printf("Retrieved object with mode '%v' named '%v'\n", fo.Mode, fo.Name)
	}
}
Output:

Object created, identifier 'some random identifier'
Retrieved object with mode 'tcp' named 'hello TCP 1'
Retrieved object with mode 'tcp' named 'hello TCP 2'
Example (Usage)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

// retrieve and create backend, handling errors along the way.
backend := lbaasv1.Backend{Identifier: "bogus identifier 1"}
if err := apiClient.Get(context.TODO(), &backend); IgnoreNotFound(err) != nil {
	fmt.Printf("Fatal error while retrieving backend: %v\n", err)
} else if err != nil {
	fmt.Printf("Backend not yet existing, creating ...\n")

	backend.Name = "backend-01"
	backend.Mode = lbaasv1.HTTP
	// [...]

	if err := apiClient.Create(context.TODO(), &backend); err != nil {
		fmt.Printf("Fatal error while creating backend: %v\n", err)
	}
} else {
	fmt.Printf("Found backend with name %v and mode %v\n", backend.Name, backend.Mode)
	fmt.Printf("Deleting it for fun and profit :)\n")

	if err := apiClient.Destroy(context.TODO(), &backend); err != nil {
		fmt.Printf("Error destroying the backend: %v", err)
	}
}
Output:

Found backend with name Example-Backend and mode tcp
Deleting it for fun and profit :)

Index

Examples

Constants

View Source
const (
	// ListChannelDefaultPageSize specifies the default page size for List operations returning the data via channel.
	ListChannelDefaultPageSize = 10
)

Variables

View Source
var (
	// ErrOperationNotSupported is returned when requesting an operation on a resource it does not support.
	ErrOperationNotSupported = errors.New("requested operation is not supported by the resource type")

	// ErrUnsupportedResponseFormat is set when the engine responds in a format we don't understand, for example unknown Content-Types.
	ErrUnsupportedResponseFormat = errors.New("response format is not supported")

	// ErrPageResponseNotSupported is returned when trying to parse a paged response and the format of the response body is not (yet) supported.
	ErrPageResponseNotSupported = fmt.Errorf("paged response invalid: %w", ErrUnsupportedResponseFormat)

	// ErrCannotListChannelAndPaged is returned when the user List()ing with the AsObjectChannel() and Paged() options set and didn't gave nil for Paged() PageInfo output argument.
	ErrCannotListChannelAndPaged = errors.New("list with Paged and ObjectChannel is only valid when not retrieving the PageInfo iterator via Paged option")

	// ErrContextRequired is returned when a nil context was passed as argument.
	ErrContextRequired = errors.New("no context given")
)
View Source
var (
	// ErrUnidentifiedObject is returned when an IdentifiedObject was required, but the passed object didn't have the identifying attribute set.
	//
	// Deprecated: moved to pkg/api/types
	ErrUnidentifiedObject = types.ErrUnidentifiedObject

	// ErrTypeNotSupported is returned when an argument is of type interface{}, manual type checking via reflection is done and the given arguments type cannot be used.
	//
	// Deprecated: moved to pkg/api/types
	ErrTypeNotSupported = types.ErrTypeNotSupported

	// ErrObjectWithoutIdentifier is a specialized ErrTypeNotSupport for Objects not having a fields tagged with `anxcloud:"identifier"`.
	//
	// Deprecated: moved to pkg/api/types
	ErrObjectWithoutIdentifier = types.ErrObjectWithoutIdentifier

	// ErrObjectWithMultipleIdentifier is a specialized ErrTypeNotSupport for Objects having multiple fields tagged with `anxcloud:"identifier"`.
	//
	// Deprecated: moved to pkg/api/types
	ErrObjectWithMultipleIdentifier = types.ErrObjectWithMultipleIdentifier

	// ErrObjectIdentifierTypeNotSupported is a specialized ErrTypeNotSupport for Objects having a field tagged with `anxcloud:"identifier"` with an unsupported type.
	//
	// Deprecated: moved to pkg/api/types
	ErrObjectIdentifierTypeNotSupported = types.ErrObjectIdentifierTypeNotSupported
)

Functions

func EnvironmentOption added in v0.7.0

func EnvironmentOption(apiGroup, envPathSegment string, override bool) types.AnyOption

EnvironmentOption can be used to configure an alternative environment path segment for a given API group

func ErrorFromResponse added in v0.4.2

func ErrorFromResponse(req *http.Request, res *http.Response) error

ErrorFromResponse creates a new HTTPError from the given response.

func GetEnvironmentPathSegment added in v0.7.0

func GetEnvironmentPathSegment(ctx context.Context, apiGroup, defaultValue string) string

GetEnvironmentPathSegment retrieves the environment path segment of a given API group or the provided defaultValue if no environment override is set

func GetObjectIdentifier deprecated added in v0.4.1

func GetObjectIdentifier(obj types.Object, singleObjectOperation bool) (string, error)

GetObjectIdentifier extracts the identifier of the given object, returning an error if no identifier field is found or singleObjectOperation is true and an identifier field is found, but empty.

Deprecated: moved to pkg/api/types

func IgnoreNotFound

func IgnoreNotFound(err error) error

IgnoreNotFound is a helper to handle ErrNotFound differently than other errors with less code.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"

	"go.anx.io/go-anxcloud/pkg/lbaas/backend"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func main() {
	api := newExampleAPI()

	backend := backend.Backend{Identifier: "non-existing identifier"}
	if err := api.Get(context.TODO(), &backend); IgnoreNotFound(err) != nil {
		fmt.Printf("Error retrieving backend from engine: %v\n", err)
	} else if err != nil {
		fmt.Printf("Requested backend does not exist\n")
	} else {
		fmt.Printf("Retrieved backend with name '%v'\n", backend.Name)
	}

}

var _ = Describe("HTTPError", func() {
	Context("when creating a HTTPError without custom message and without wrapping an error", func() {
		var err error

		BeforeEach(func() {
			req := httptest.NewRequest("GET", "/", nil)
			rec := httptest.NewRecorder()
			rec.WriteHeader(500)

			err = newHTTPError(req, rec.Result(), nil, nil)

			he := HTTPError{
				message:    "Engine returned an error: 500 Internal Server Error (500)",
				statusCode: 500,
				url: &url.URL{
					Path: "/",
				},
				method: "GET",
			}

			Expect(err).To(MatchError(he))
		})

		It("it returns the status code", func() {
			var he HTTPError
			Expect(errors.As(err, &he)).To(BeTrue())
			Expect(he.StatusCode()).To(Equal(500))
		})

		It("it returns the expected message", func() {
			var he HTTPError
			Expect(errors.As(err, &he)).To(BeTrue())
			Expect(he.Error()).To(Equal("Engine returned an error: 500 Internal Server Error (500)"))
		})

		It("it does not wrap an EngineError", func() {
			var ee EngineError
			Expect(errors.As(err, &ee)).To(BeFalse())
		})
	})

	Context("when creating a HTTPError with wrapping an EngineError", func() {
		var err error

		BeforeEach(func() {
			req := httptest.NewRequest("GET", "/", nil)
			rec := httptest.NewRecorder()
			rec.WriteHeader(500)

			err = newHTTPError(req, rec.Result(), ErrNotFound, nil)
		})

		It("it wraps the given EngineError error", func() {
			Expect(err).To(MatchError(ErrNotFound))
		})
	})

	Context("when creating a HTTPError with a custom error message", func() {
		var err error

		BeforeEach(func() {
			req := httptest.NewRequest("GET", "/", nil)
			rec := httptest.NewRecorder()
			rec.WriteHeader(500)

			msg := "Random message for testing"
			err = newHTTPError(req, rec.Result(), nil, &msg)
		})

		It("it returns the correct message", func() {
			Expect(err.Error()).To(Equal("Random message for testing"))
		})
	})
})

var _ = Describe("ErrorFromResponse function", func() {
	req := httptest.NewRequest("GET", "/", nil)

	var statusCode int
	var res *http.Response

	JustBeforeEach(func() {
		rec := httptest.NewRecorder()
		rec.WriteHeader(statusCode)
		res = rec.Result()
	})

	Context("for status code 404", func() {
		BeforeEach(func() {
			statusCode = 404
		})

		It("returns ErrNotFound as expected", func() {
			err := ErrorFromResponse(req, res)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ErrNotFound))
		})
	})

	Context("for status code 403", func() {
		BeforeEach(func() {
			statusCode = 403
		})

		It("returns ErrAccessDenied as expected", func() {
			err := ErrorFromResponse(req, res)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ErrAccessDenied))
		})
	})

	Context("for status code 500", func() {
		BeforeEach(func() {
			statusCode = 500
		})

		It("returns a matching HTTPError as expected", func() {
			err := ErrorFromResponse(req, res)
			Expect(err).To(HaveOccurred())

			var he HTTPError
			ok := errors.As(err, &he)
			Expect(ok).To(BeTrue())

			Expect(he.StatusCode()).To(Equal(500))
		})
	})
})

var _ = Describe("EngineError", func() {
	It("returns the correct message", func() {
		Expect(ErrNotFound.Error()).To(Equal("requested resource does not exist on the engine"))
	})

	Context("when created without a wrapping error", func() {
		It("does not return a wrapped error", func() {
			Expect(ErrNotFound.Unwrap()).To(Succeed())
		})
	})
})
Output:

Requested backend does not exist

func NewHTTPError added in v0.4.2

func NewHTTPError(status int, method string, url *url.URL, wrapped error) error

NewHTTPError creates a new HTTPError instance with the given values, which is mostly useful for mock-testing.

Types

type API

type API types.API

API is the interface to perform operations on the engine.

Example (Create)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

backend := lbaasv1.Backend{
	Name: "backend-01",
	Mode: lbaasv1.HTTP,
	// [...]
}

if err := apiClient.Create(context.TODO(), &backend); err != nil {
	fmt.Printf("Error creating backend: %v\n", err)
} else {
	fmt.Printf("Created backend '%v', engine assigned identifier '%v'\n", backend.Name, backend.Identifier)
}
Output:

Created backend 'backend-01', engine assigned identifier 'generated identifier 1'
Example (Destroy)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

backend := lbaasv1.Backend{Identifier: "bogus identifier 1"}
if err := apiClient.Destroy(context.TODO(), &backend); err != nil {
	fmt.Printf("Error destroying backend: %v\n", err)
} else {
	fmt.Printf("Successfully destroyed backend\n")
}
Output:

Successfully destroyed backend
Example (Get)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

backend := lbaasv1.Backend{Identifier: "bogus identifier 1"}
if err := apiClient.Get(context.TODO(), &backend); err != nil {
	fmt.Printf("Error retrieving backend: %v\n", err)
} else {
	fmt.Printf("Got backend named \"%v\"\n", backend.Name)
}
Output:

Got backend named "Example-Backend"
Example (ListChannel)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

var channel types.ObjectChannel

// list all backends using a channel and have the library handle the paging.
// Oh and we filter by LoadBalancer, because we can and the example has to be somewhere.

// Beware: listing endpoints usually do not return all data for an object, sometimes
// only the identifier is filled. This varies by specific API. If you need full objects,
// the FullObjects option might be your friend.
b := lbaasv1.Backend{LoadBalancer: lbaasv1.LoadBalancer{Identifier: "bogus identifier 2"}}
if err := apiClient.List(context.TODO(), &b, ObjectChannel(&channel)); err != nil {
	fmt.Printf("Error listing backends: %v\n", err)
} else {
	for res := range channel {
		if err = res(&b); err != nil {
			fmt.Printf("Error retrieving backend from channel: %v\n", err)
			break
		}

		// b.Mode is only filled when the full object is retrieved since this attribute is
		// not returned by the List API endpoint. To have it, we would have to either manually
		// retrieve the full object or use the FullObjects option in the List call above.
		// See the ListPaged example for the FullObjects option in action.
		fmt.Printf("Got backend named \"%v\" with mode \"%v\"\n", b.Name, b.Mode)
	}
}
Output:

Got backend named "Example-Backend" with mode ""
Got backend named "test-backend-02" with mode ""
Got backend named "test-backend-04" with mode ""
Example (ListPaged)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

// List all backends, with 10 entries per page and starting on first page.

// Beware: listing endpoints usually do not return all data for an object, sometimes
// only the identifier is filled. This varies by specific API. If you need full objects,
// the FullObjects option might be your friend. To test this option, we use it here.
b := lbaasv1.Backend{}
var pageIter types.PageInfo
if err := apiClient.List(context.TODO(), &b, Paged(1, 2, &pageIter), FullObjects(true)); err != nil {
	fmt.Printf("Error listing backends: %v\n", err)
} else {
	var backends []lbaasv1.Backend
	for pageIter.Next(&backends) {
		fmt.Printf("Listing entries on page %v\n", pageIter.CurrentPage())

		for _, backend := range backends {
			// backend.Mode is only filled when the full object is retrieved, we can only use it here because
			// we added the FullObjects(true) option to the List() call above.
			fmt.Printf("  Got backend named \"%v\" with mode \"%v\"\n", backend.Name, backend.Mode)
		}
	}

	if err := pageIter.Error(); err != nil {
		// Handle error catched while iterating pages.
		// Errors will prevent pageIter.Next() to continue, you can call pageIter.ResetError() to resume.
		fmt.Printf("Error while iterating pages of backends: %v\n", err)
	}

	fmt.Printf("Last page listed was page %v, which returned %v entries\n", pageIter.CurrentPage(), len(backends))
}
Output:

Listing entries on page 1
  Got backend named "Example-Backend" with mode "tcp"
  Got backend named "backend-01" with mode "tcp"
Listing entries on page 2
  Got backend named "test-backend-01" with mode "tcp"
  Got backend named "test-backend-02" with mode "tcp"
Listing entries on page 3
  Got backend named "test-backend-03" with mode "tcp"
  Got backend named "test-backend-04" with mode "tcp"
Last page listed was page 4, which returned 0 entries
Example (Update)
// see example on NewAPI how to implement this function
apiClient := newExampleAPI()

b := lbaasv1.Backend{
	Identifier: "bogus identifier 1",
	Name:       "Updated backend",
	Mode:       lbaasv1.HTTP,
	// [...]
}

if err := apiClient.Update(context.TODO(), &b); err != nil {
	fmt.Printf("Error updating backend: %v\n", err)
} else {
	fmt.Printf("Successfully updated backend\n")

	retrieved := lbaasv1.Backend{Identifier: "bogus identifier 1"}
	if err := apiClient.Get(context.TODO(), &retrieved); err != nil {
		fmt.Printf("Error verifying updated backend: %v\n", err)
	} else {
		fmt.Printf("Backend is now renamed to '%v' and has mode %v\n", retrieved.Name, retrieved.Mode)
	}
}
Output:

Successfully updated backend
Backend is now renamed to 'Updated backend' and has mode http

func NewAPI

func NewAPI(opts ...NewAPIOption) (API, error)

NewAPI creates a new API client which implements the API interface.

Example
api, err := NewAPI(
	// you might find client.TokenFromEnv(false) useful
	WithClientOptions(client.TokenFromString("bogus auth token")),
)

if err != nil {
	log.Fatalf("Error creating api instance: %v\n", err)
} else {
	// do something with api
	lb := lbaasv1.LoadBalancer{Identifier: "bogus identifier"}
	if err := api.Get(context.TODO(), &lb); IgnoreNotFound(err) != nil {
		fmt.Printf("Error retrieving loadbalancer with identifier '%v'\n", lb.Identifier)
	}
}

// fails because we didn't pass a valid auth token nor a valid identifier
Output:

Error retrieving loadbalancer with identifier 'bogus identifier'

type CreateOption

type CreateOption = types.CreateOption

CreateOption is the interface options have to implement to be usable with Create operation. Re-exported from pkg/api/types.

func AutoTag added in v0.4.6

func AutoTag(tags ...string) CreateOption

AutoTag can be used to automatically tag objects after creation

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"sort"
	"strings"

	"go.anx.io/go-anxcloud/pkg/api"
	"go.anx.io/go-anxcloud/pkg/api/mock"

	vlanv1 "go.anx.io/go-anxcloud/pkg/apis/vlan/v1"
)

func main() {
	a := mock.NewMockAPI()

	vlan := vlanv1.VLAN{DescriptionCustomer: "mocked VLAN"}
	if err := a.Create(context.TODO(), &vlan, api.AutoTag("foo", "bar", "baz")); err != nil {
		taggingErr := &api.ErrTaggingFailed{}
		if errors.As(err, taggingErr) {
			log.Fatalf("object successfully created but tagging failed: %s", taggingErr.Error())
		} else {
			log.Fatalf("unknown error occurred: %s", err)
		}
	}

	// Note that `a.Inspect` is only available when using the mock client implementation.
	tags := a.Inspect(vlan.Identifier).Tags()
	sort.Strings(tags)

	fmt.Println(strings.Join(tags, ", "))
}
Output:

bar, baz, foo

type DestroyOption

type DestroyOption = types.DestroyOption

DestroyOption is the interface options have to implement to be usable with Destroy operation. Re-exported from pkg/api/types.

type EngineError

type EngineError struct {
	// contains filtered or unexported fields
}

EngineError is the base type for all errors returned by the engine.

Ideally all errors returned by the API are transformed into EngineErrors, making HTTPError obsolete, as this would completely decouple communicating with the Engine from using HTTP.

var (
	// ErrNotFound is returned when the given identified object does not exist in the engine. Take a look at IgnoreNotFound(), too.
	ErrNotFound EngineError = EngineError{/* contains filtered or unexported fields */}

	// ErrAccessDenied is returned when the used authentication credential is not authorized to do the requested operation.
	ErrAccessDenied EngineError = EngineError{/* contains filtered or unexported fields */}
)

func (EngineError) Error

func (e EngineError) Error() string

Error returns the message of the EngineError, implementing the `error` interface.

func (EngineError) Unwrap

func (e EngineError) Unwrap() error

Unwrap returns the wrapped error of the EngineError, making it compatible with `errors.Is/As/Unwrap`.

type ErrTaggingFailed added in v0.4.6

type ErrTaggingFailed struct {
	// contains filtered or unexported fields
}

ErrTaggingFailed is returned when resource tagging failed

func (ErrTaggingFailed) Error added in v0.4.6

func (e ErrTaggingFailed) Error() string

Error returns the error message.

func (ErrTaggingFailed) Unwrap added in v0.4.6

func (e ErrTaggingFailed) Unwrap() error

Unwrap returns the error which caused this one.

type GetOption

type GetOption = types.GetOption

GetOption is the interface options have to implement to be usable with Get operation. Re-exported from pkg/api/types.

type HTTPError

type HTTPError struct {
	// contains filtered or unexported fields
}

HTTPError is an not-specially-implemented EngineError for a given status code. Ideally this is not used because every returned error is mapped to an ErrSomething package variable, decoupling error handling from the transport protocol.

func (HTTPError) Error

func (e HTTPError) Error() string

Error returns the error message.

func (HTTPError) StatusCode

func (e HTTPError) StatusCode() int

StatusCode returns the HTTP status code of the HTTPError.

func (HTTPError) Unwrap

func (e HTTPError) Unwrap() error

Unwrap returns the error which caused this one.

type ListOption

type ListOption = types.ListOption

ListOption is the interface options have to implement to be usable with List operation. Re-exported from pkg/api/types.

func FullObjects

func FullObjects(fullObjects bool) ListOption

FullObjects can be set to make a Get for every object before it is returned to the caller of List(). This is necessary since most API endpoints for listing objects only return a subset of their data.

Beware: this makes one API call to retrieve the objects (ok, one call per page of objects) and an additional call per object. Because of this being very slow, it is an optional feature and should only be used with care.

func ObjectChannel

func ObjectChannel(channel *types.ObjectChannel) ListOption

ObjectChannel configures the List operation to return the objects via the given channel. When listing via channel you either have to read until the channel is closed or pass a context you cancel explicitly - failing to do that will result in leaked goroutines.

func Paged

func Paged(page, limit uint, info *types.PageInfo) ListOption

Paged is an option valid for List operations to retrieve objects in a paged fashion (instead of all at once).

type NewAPIOption

type NewAPIOption func(*defaultAPI)

NewAPIOption is the type for giving options to the NewAPI function.

func WithClientOptions

func WithClientOptions(o ...client.Option) NewAPIOption

WithClientOptions configures the API to pass the given client.Option to the client when creating it.

func WithLogger

func WithLogger(l logr.Logger) NewAPIOption

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 be given to the client (with added name "client").

func WithRequestOptions added in v0.7.0

func WithRequestOptions(opts ...types.Option) NewAPIOption

WithRequestOptions configures default options applied to requests

Example
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)
}
Output:

type UpdateOption

type UpdateOption = types.UpdateOption

UpdateOption is the interface options have to implement to be usable with Update operation. Re-exported from pkg/api/types.

Directories

Path Synopsis
Package types contains everything needed by APIs implementing the interfaces to be compatible with the generic API client.
Package types contains everything needed by APIs implementing the interfaces to be compatible with the generic API client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL