From e22cc7ef4b3950506a29b74d3964b9282d7c4dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Sch=C3=A4fer?= Date: Mon, 30 Oct 2023 13:19:38 +0100 Subject: [PATCH] LBAAS-142: add lbaas/v2 bindings --- CHANGELOG.md | 1 + pkg/apis/common/gs/common.go | 49 +++++ pkg/apis/lbaas/v2/common.go | 7 + pkg/apis/lbaas/v2/lb_cluster_genclient.go | 24 +++ pkg/apis/lbaas/v2/lb_cluster_types.go | 26 +++ pkg/apis/lbaas/v2/lb_node_genclient.go | 24 +++ pkg/apis/lbaas/v2/lb_node_types.go | 19 ++ pkg/apis/lbaas/v2/load_balancer_genclient.go | 24 +++ pkg/apis/lbaas/v2/load_balancer_types.go | 120 ++++++++++++ pkg/apis/lbaas/v2/suite_test.go | 181 +++++++++++++++++++ pkg/apis/lbaas/v2/xxgenerated_object.go | 22 +++ pkg/apis/lbaas/v2/xxgenerated_object_test.go | 47 +++++ 12 files changed, 544 insertions(+) create mode 100644 pkg/apis/lbaas/v2/common.go create mode 100644 pkg/apis/lbaas/v2/lb_cluster_genclient.go create mode 100644 pkg/apis/lbaas/v2/lb_cluster_types.go create mode 100644 pkg/apis/lbaas/v2/lb_node_genclient.go create mode 100644 pkg/apis/lbaas/v2/lb_node_types.go create mode 100644 pkg/apis/lbaas/v2/load_balancer_genclient.go create mode 100644 pkg/apis/lbaas/v2/load_balancer_types.go create mode 100644 pkg/apis/lbaas/v2/suite_test.go create mode 100644 pkg/apis/lbaas/v2/xxgenerated_object.go create mode 100644 pkg/apis/lbaas/v2/xxgenerated_object_test.go 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..34cdf767 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,50 @@ func (gs *GenericService) FilterAPIResponse(ctx context.Context, res *http.Respo } return res, nil } + +// 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/common.go b/pkg/apis/lbaas/v2/common.go new file mode 100644 index 00000000..665c1bf6 --- /dev/null +++ b/pkg/apis/lbaas/v2/common.go @@ -0,0 +1,7 @@ +package v2 + +// 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"` +} 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..daa6f981 --- /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 { + 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/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..0c093d97 --- /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 { + 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/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..0950d4ab --- /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 { + 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/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..7ff614b1 --- /dev/null +++ b/pkg/apis/lbaas/v2/load_balancer_types.go @@ -0,0 +1,120 @@ +package v2 + +import ( + "encoding/json" + + "go.anx.io/go-anxcloud/pkg/apis/common" + "go.anx.io/go-anxcloud/pkg/apis/common/gs" +) + +// 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"` + // Definition onfigures the load balancer's frontends and backends. + // This field is currently unstable and requires an update of go-anxcloud + // in the near future. + Definition *Definition `json:"definition,omitempty"` +} + +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"` +} + +type Definition definition + +func (d *Definition) MarshalJSON() ([]byte, error) { + inner := definition(*d) + + data, err := json.Marshal(&inner) + if err != nil { + return nil, err + } + + return json.Marshal(string(data)) +} + +func (d *Definition) UnmarshalJSON(data []byte) error { + var innerString string + if err := json.Unmarshal(data, &innerString); err != nil { + return err + } + + var inner definition + if err := json.Unmarshal([]byte(innerString), &inner); err != nil { + return err + } + + *d = Definition(inner) + + return nil +} + +// 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..ee8fd767 --- /dev/null +++ b/pkg/apis/lbaas/v2/suite_test.go @@ -0,0 +1,181 @@ +package v2 + +import ( + "context" + "encoding/json" + + "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" + + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +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/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/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/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/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/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/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/v1/load_balancers.json"), + ghttp.VerifyJSON(`{ + "name": "foo", + "generation": 5, + "cluster": "bar", + "frontend_ips": "foo,bar", + "ssl_certificates": "bar,foo", + "definition": "{\"frontends\":[{\"name\":\"foo\",\"protocol\":\"TCP\",\"backend\":{\"protocol\":\"TCP\"}}]}" + }`), + 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"}}, + Definition: &Definition{Frontends: []Frontend{{Name: "foo", Protocol: "TCP", Backend: FrontendBackend{Protocol: "TCP"}}}}, + } + 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/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 decoding nested json (Definition)", func() { + lbJson := `{"definition": "{\"frontends\":[{\"name\":\"foo\",\"protocol\":\"TCP\",\"backend\":{\"protocol\":\"TCP\"}}]}"}` + var lb LoadBalancer + err := json.Unmarshal([]byte(lbJson), &lb) + Expect(err).ToNot(HaveOccurred()) + Expect(*lb.Definition).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...) +})