diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc3c951..70699c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Some examples, more below in the actual changelog (newer entries are more likely ### Added * core/v1: add `created_at` and `updated_at` fields to Resource type * generic client: make common.PartialResource filterable +* lbaas/v2: added new `Cluster`, `Node` & `LoadBalancer` resources (#309, @anx-mschaefer) ## [0.5.3] - 2023-06-16 diff --git a/pkg/apis/common/gs/common.go b/pkg/apis/common/gs/common.go index 30f988c1..c127e8fc 100644 --- a/pkg/apis/common/gs/common.go +++ b/pkg/apis/common/gs/common.go @@ -3,8 +3,10 @@ package gs import ( "bytes" "context" + "go.anx.io/go-anxcloud/pkg/utils/object/filter" "io" "net/http" + "net/url" "go.anx.io/go-anxcloud/pkg/api/types" ) @@ -30,3 +32,56 @@ func (gs *GenericService) FilterAPIResponse(ctx context.Context, res *http.Respo } return res, nil } + +// CommonRequestBody is used to optionally omit the `State` field on create and update +// by embedding it to the request in the FilterAPIRequestBody hook +type CommonRequestBody struct { + State string `json:"state,omitempty"` +} + +// RequestBody prevents decoding of delete responses as they are not compatible with the +// objects type +func RequestBody(ctx context.Context, br func() interface{}) (interface{}, error) { + op, err := types.OperationFromContext(ctx) + if err != nil { + return nil, err + } + + if op == types.OperationCreate || op == types.OperationUpdate { + response := br() + + return response, nil + } + + return nil, nil +} + +// EndpointURL is a helper function which can be wrapped by API bindings to enable the filter helper +func EndpointURL(ctx context.Context, obj types.Object, resourcePath string) (*url.URL, error) { + op, err := types.OperationFromContext(ctx) + if err != nil { + return nil, err + } + + u, err := url.Parse(resourcePath) + if err != nil { + return nil, err + } + + if op == types.OperationList { + helper, err := filter.NewHelper(obj) + if err != nil { + return nil, err + } + + filters := helper.BuildQuery().Encode() + + if filters != "" { + query := u.Query() + query.Set("filters", filters) + u.RawQuery = query.Encode() + } + } + + return u, nil +} diff --git a/pkg/apis/lbaas/v2/lb_cluster_genclient.go b/pkg/apis/lbaas/v2/lb_cluster_genclient.go new file mode 100644 index 00000000..9471632c --- /dev/null +++ b/pkg/apis/lbaas/v2/lb_cluster_genclient.go @@ -0,0 +1,24 @@ +package v2 + +import ( + "context" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" + "net/url" +) + +// FilterAPIRequestBody adds the CommonRequestBody +func (c *Cluster) FilterAPIRequestBody(ctx context.Context) (interface{}, error) { + return gs.RequestBody(ctx, func() interface{} { + return &struct { + gs.CommonRequestBody + Cluster + }{ + Cluster: *c, + } + }) +} + +// EndpointURL returns the common URL for operations on the Cluster resource +func (c *Cluster) EndpointURL(ctx context.Context) (*url.URL, error) { + return gs.EndpointURL(ctx, c, "/api/LBaaSv2_DEV/v1/clusters.json") +} diff --git a/pkg/apis/lbaas/v2/lb_cluster_types.go b/pkg/apis/lbaas/v2/lb_cluster_types.go new file mode 100644 index 00000000..702496f6 --- /dev/null +++ b/pkg/apis/lbaas/v2/lb_cluster_types.go @@ -0,0 +1,26 @@ +package v2 + +import ( + "go.anx.io/go-anxcloud/pkg/apis/common/gs" +) + +type LoadBalancerImplementation string + +const ( + LoadBalancerImplementationHAProxy LoadBalancerImplementation = "haproxy" +) + +// anxcloud:object + +// Cluster holds the information of a load balancing cluster +type Cluster struct { + gs.GenericService + gs.HasState + + Identifier string `json:"identifier,omitempty" anxcloud:"identifier"` + Name string `json:"name,omitempty"` + Implementation LoadBalancerImplementation `json:"implementation,omitempty"` + FrontendPrefixes *gs.PartialResourceList `json:"frontend_prefixes,omitempty"` + BackendPrefixes *gs.PartialResourceList `json:"backend_prefixes,omitempty"` + Replicas *int `json:"replicas,omitempty"` +} diff --git a/pkg/apis/lbaas/v2/lb_node_genclient.go b/pkg/apis/lbaas/v2/lb_node_genclient.go new file mode 100644 index 00000000..e90e07d7 --- /dev/null +++ b/pkg/apis/lbaas/v2/lb_node_genclient.go @@ -0,0 +1,24 @@ +package v2 + +import ( + "context" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" + "net/url" +) + +// FilterAPIRequestBody adds the CommonRequestBody +func (n *Node) FilterAPIRequestBody(ctx context.Context) (interface{}, error) { + return gs.RequestBody(ctx, func() interface{} { + return &struct { + gs.CommonRequestBody + Node + }{ + Node: *n, + } + }) +} + +// EndpointURL returns the common URL for operations on the Node resource +func (n *Node) EndpointURL(ctx context.Context) (*url.URL, error) { + return gs.EndpointURL(ctx, n, "/api/LBaaSv2_DEV/v1/nodes.json") +} diff --git a/pkg/apis/lbaas/v2/lb_node_types.go b/pkg/apis/lbaas/v2/lb_node_types.go new file mode 100644 index 00000000..d580b522 --- /dev/null +++ b/pkg/apis/lbaas/v2/lb_node_types.go @@ -0,0 +1,19 @@ +package v2 + +import ( + "go.anx.io/go-anxcloud/pkg/apis/common" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" +) + +// anxcloud:object + +// Node holds the information of a load balancing node within a Cluster +type Node struct { + gs.GenericService + gs.HasState + + Identifier string `json:"identifier,omitempty" anxcloud:"identifier"` + Name string `json:"name,omitempty"` + VM *common.PartialResource `json:"vm,omitempty"` + Cluster *common.PartialResource `json:"cluster,omitempty" anxcloud:"filterable"` +} diff --git a/pkg/apis/lbaas/v2/load_balancer_genclient.go b/pkg/apis/lbaas/v2/load_balancer_genclient.go new file mode 100644 index 00000000..3b3eb985 --- /dev/null +++ b/pkg/apis/lbaas/v2/load_balancer_genclient.go @@ -0,0 +1,24 @@ +package v2 + +import ( + "context" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" + "net/url" +) + +// FilterAPIRequestBody adds the CommonRequestBody +func (lb *LoadBalancer) FilterAPIRequestBody(ctx context.Context) (interface{}, error) { + return gs.RequestBody(ctx, func() interface{} { + return &struct { + gs.CommonRequestBody + LoadBalancer + }{ + LoadBalancer: *lb, + } + }) +} + +// EndpointURL returns the common URL for operations on LoadBalancer resource +func (lb *LoadBalancer) EndpointURL(ctx context.Context) (*url.URL, error) { + return gs.EndpointURL(ctx, lb, "/api/LBaaSv2_DEV/v1/load_balancers.json") +} diff --git a/pkg/apis/lbaas/v2/load_balancer_types.go b/pkg/apis/lbaas/v2/load_balancer_types.go new file mode 100644 index 00000000..6bea5a7b --- /dev/null +++ b/pkg/apis/lbaas/v2/load_balancer_types.go @@ -0,0 +1,105 @@ +package v2 + +import ( + "encoding/json" + + "go.anx.io/go-anxcloud/pkg/apis/common" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" + "go.anx.io/go-anxcloud/pkg/utils/pointer" +) + +// anxcloud:object + +// LoadBalancer holds the information of a load balancing configuration within a Cluster +type LoadBalancer struct { + gs.GenericService + gs.HasState + + Identifier string `json:"identifier,omitempty" anxcloud:"identifier"` + Name string `json:"name,omitempty"` + Generation int `json:"generation,omitempty"` + Cluster *common.PartialResource `json:"cluster,omitempty" anxcloud:"filterable"` + FrontendIPs *gs.PartialResourceList `json:"frontend_ips,omitempty"` + SSLCertificates *gs.PartialResourceList `json:"ssl_certificates,omitempty"` + RawDefinition *string `json:"definition,omitempty"` +} + +// GetDefinition handles double json decoding. +// The API will likely allow nested json in the future. +func (lb *LoadBalancer) GetDefinition() (def Definition, err error) { + err = json.Unmarshal([]byte(*lb.RawDefinition), &def) + return +} + +// SetDefinition handles double json encoding. +// The API will likely allow nested json in the future. +func (lb *LoadBalancer) SetDefinition(def *Definition) (err error) { + var rawDef []byte + rawDef, err = json.Marshal(def) + lb.RawDefinition = pointer.String(string(rawDef)) + return +} + +type FrontendProtocol string + +const ( + FrontendProtocolTCP FrontendProtocol = "TCP" +) + +type BackendProtocol string + +const ( + BackendProtocolTCP BackendProtocol = "TCP" + BackendProtocolPROXY BackendProtocol = "PROXY" +) + +type Definition struct { + Frontends []Frontend `json:"frontends,omitempty"` + Backends []Backend `json:"backends,omitempty"` +} + +// Define ports and protocols exposed to the public side of the Load Balancer +type Frontend struct { + // Name of the Frontend + Name string `json:"name"` + // Frontend service protocol + Protocol FrontendProtocol `json:"protocol"` + // Configure frontend - backend relation + Backend FrontendBackend `json:"backend,omitempty"` + // TCP specific frontend configuration + TCP *FrontendTCP `json:"tcp,omitempty"` +} + +// TCP specific frontend configuration +type FrontendTCP struct { + // Port for the frontend to listen to + Port uint16 `json:"port,omitempty"` +} + +// Configure frontend - backend relation +type FrontendBackend struct { + // Backend service protocol + Protocol BackendProtocol `json:"protocol"` + // TCP specific backend configuration + TCP *FrontendBackendTCP `json:"tcp,omitempty"` +} + +// TCP specific backend configuration +type FrontendBackendTCP struct { + Port uint16 `json:"port"` +} + +// Define backend services and connect them to frontends +type Backend struct { + // Name of the Backend + Name string `json:"name"` + // IP addresses of the backend service + IPs []string `json:"ips,omitempty"` + // List of frontends connected to the backend + Frontends []BackendFrontend `json:"frontends,omitempty"` +} + +type BackendFrontend struct { + // Name of the frontend to be connected to the backend + Name string `json:"name"` +} diff --git a/pkg/apis/lbaas/v2/suite_test.go b/pkg/apis/lbaas/v2/suite_test.go new file mode 100644 index 00000000..5a68279d --- /dev/null +++ b/pkg/apis/lbaas/v2/suite_test.go @@ -0,0 +1,182 @@ +package v2 + +import ( + "context" + "github.com/onsi/gomega/ghttp" + "go.anx.io/go-anxcloud/pkg/api" + "go.anx.io/go-anxcloud/pkg/api/types" + "go.anx.io/go-anxcloud/pkg/apis/common" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" + "go.anx.io/go-anxcloud/pkg/client" + "go.anx.io/go-anxcloud/pkg/utils/pointer" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "testing" +) + +func TestLBaaSv2APIBindings(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "LBaaS v2 API Bindings Suite") +} + +var _ = Describe("mock", func() { + var ( + engine api.API + srv *ghttp.Server + ) + + BeforeEach(func() { + var err error + srv = ghttp.NewServer() + engine, err = api.NewAPI( + api.WithClientOptions( + client.BaseURL(srv.URL()), + client.IgnoreMissingToken(), + ), + ) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + srv.Close() + }) + + Context("Cluster", func() { + It("correctly encodes zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/clusters.json"), + ghttp.VerifyJSON(`{}`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + c := Cluster{} + err := engine.Create(context.TODO(), &c) + Expect(err).ToNot(HaveOccurred()) + }) + + It("correctly encodes non-zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/clusters.json"), + ghttp.VerifyJSON(`{ + "name": "foo", + "implementation": "haproxy", + "frontend_prefixes": "foo,bar", + "backend_prefixes": "bar,foo", + "replicas": 3 + }`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + c := Cluster{ + Name: "foo", + Implementation: LoadBalancerImplementationHAProxy, + FrontendPrefixes: &gs.PartialResourceList{{Identifier: "foo"}, {Identifier: "bar"}}, + BackendPrefixes: &gs.PartialResourceList{{Identifier: "bar"}, {Identifier: "foo"}}, + Replicas: pointer.Int(3), + } + err := engine.Create(context.TODO(), &c) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Node", func() { + It("correctly encodes zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/nodes.json"), + ghttp.VerifyJSON(`{}`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + n := Node{} + err := engine.Create(context.TODO(), &n) + Expect(err).ToNot(HaveOccurred()) + }) + + It("correctly encodes non-zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/nodes.json"), + ghttp.VerifyJSON(`{ + "name": "foo", + "vm": "foo", + "cluster": "bar" + }`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + n := Node{ + Name: "foo", + VM: &common.PartialResource{Identifier: "foo"}, + Cluster: &common.PartialResource{Identifier: "bar"}, + } + err := engine.Create(context.TODO(), &n) + Expect(err).ToNot(HaveOccurred()) + }) + + It("correctly applies filter parameters on List operations", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/LBaaSv2_DEV/v1/nodes.json", "limit=10&page=1&filters=cluster=bar"), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + n := Node{Cluster: &common.PartialResource{Identifier: "bar"}} + var channel types.ObjectChannel + err := engine.List(context.TODO(), &n, api.ObjectChannel(&channel)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("LoadBalancer", func() { + It("correctly encodes zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/load_balancers.json"), + ghttp.VerifyJSON(`{}`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + lb := LoadBalancer{} + err := engine.Create(context.TODO(), &lb) + Expect(err).ToNot(HaveOccurred()) + }) + + It("correctly encodes non-zero values", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/LBaaSv2_DEV/v1/load_balancers.json"), + ghttp.VerifyJSON(`{ + "name": "foo", + "generation": 5, + "cluster": "bar", + "frontend_ips": "foo,bar", + "ssl_certificates": "bar,foo", + "definition": "baz" + }`), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + lb := LoadBalancer{ + Name: "foo", + Generation: 5, + Cluster: &common.PartialResource{Identifier: "bar"}, + FrontendIPs: &gs.PartialResourceList{{Identifier: "foo"}, {Identifier: "bar"}}, + SSLCertificates: &gs.PartialResourceList{{Identifier: "bar"}, {Identifier: "foo"}}, + RawDefinition: pointer.String("baz"), + } + err := engine.Create(context.TODO(), &lb) + Expect(err).ToNot(HaveOccurred()) + }) + It("correctly applies filter parameters on List operations", func() { + srv.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/LBaaSv2_DEV/v1/load_balancers.json", "limit=10&page=1&filters=cluster=bar"), + ghttp.RespondWithJSONEncoded(200, map[string]any{}), + )) + lb := LoadBalancer{Cluster: &common.PartialResource{Identifier: "bar"}} + var channel types.ObjectChannel + err := engine.List(context.TODO(), &lb, api.ObjectChannel(&channel)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("supports setting the definition via SetDefinition", func() { + lb := LoadBalancer{} + err := lb.SetDefinition(&Definition{Frontends: []Frontend{{Name: "foo", Protocol: "TCP", Backend: FrontendBackend{Protocol: "TCP"}}}}) + Expect(err).ToNot(HaveOccurred()) + Expect(*lb.RawDefinition).To(MatchJSON(`{"frontends":[{"name":"foo","protocol":"TCP","backend":{"protocol":"TCP"}}]}`)) + }) + It("supports getting the definition via GetDefinition", func() { + lb := LoadBalancer{RawDefinition: pointer.String(`{"frontends":[{"name":"foo","protocol":"TCP","backend":{"protocol":"TCP"}}]}`)} + def, err := lb.GetDefinition() + Expect(err).ToNot(HaveOccurred()) + Expect(def).To(Equal(Definition{Frontends: []Frontend{{Name: "foo", Protocol: "TCP", Backend: FrontendBackend{Protocol: "TCP"}}}})) + }) + }) +}) diff --git a/pkg/apis/lbaas/v2/xxgenerated_object.go b/pkg/apis/lbaas/v2/xxgenerated_object.go new file mode 100644 index 00000000..f53352b1 --- /dev/null +++ b/pkg/apis/lbaas/v2/xxgenerated_object.go @@ -0,0 +1,22 @@ +// Code generated by go.anx.io/go-anxcloud/tools object-generator - DO NOT EDIT! + +package v2 + +import ( + "context" +) + +// GetIdentifier returns the primary identifier of a Cluster object +func (o *Cluster) GetIdentifier(ctx context.Context) (string, error) { + return o.Identifier, nil +} + +// GetIdentifier returns the primary identifier of a Node object +func (o *Node) GetIdentifier(ctx context.Context) (string, error) { + return o.Identifier, nil +} + +// GetIdentifier returns the primary identifier of a LoadBalancer object +func (o *LoadBalancer) GetIdentifier(ctx context.Context) (string, error) { + return o.Identifier, nil +} diff --git a/pkg/apis/lbaas/v2/xxgenerated_object_test.go b/pkg/apis/lbaas/v2/xxgenerated_object_test.go new file mode 100644 index 00000000..de369db3 --- /dev/null +++ b/pkg/apis/lbaas/v2/xxgenerated_object_test.go @@ -0,0 +1,47 @@ +// Code generated by go.anx.io/go-anxcloud/tools object-generator - DO NOT EDIT! + +package v2_test + +import ( + . "github.com/onsi/ginkgo/v2" + testutils "go.anx.io/go-anxcloud/pkg/utils/test" + + "go.anx.io/go-anxcloud/pkg/api/types" + apipkg "go.anx.io/go-anxcloud/pkg/apis/lbaas/v2" +) + +var _ = Describe("Object Cluster", func() { + o := apipkg.Cluster{} + + ifaces := make([]interface{}, 0, 1) + { + var i types.Object + ifaces = append(ifaces, &i) + } + + testutils.ObjectTests(&o, ifaces...) +}) + +var _ = Describe("Object Node", func() { + o := apipkg.Node{} + + ifaces := make([]interface{}, 0, 1) + { + var i types.Object + ifaces = append(ifaces, &i) + } + + testutils.ObjectTests(&o, ifaces...) +}) + +var _ = Describe("Object LoadBalancer", func() { + o := apipkg.LoadBalancer{} + + ifaces := make([]interface{}, 0, 1) + { + var i types.Object + ifaces = append(ifaces, &i) + } + + testutils.ObjectTests(&o, ifaces...) +})