Skip to content

Commit c6db3bf

Browse files
Merge pull request #189 from kuudori/HYPERFLEET-1086
HYPERFLEET-1086 - feat: Channel and Version CRUD handlers
2 parents afba37e + 4fa55f8 commit c6db3bf

16 files changed

Lines changed: 1354 additions & 6 deletions

File tree

cmd/hyperfleet-api/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import (
1717
// Import plugins to trigger their init() functions
1818
// _ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/events" // REMOVED: Events plugin no longer exists
1919
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/adapterStatus"
20+
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/channels"
2021
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/clusters"
2122
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/generic"
2223
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/nodePools"
2324
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/resources"
25+
_ "github.com/openshift-hyperfleet/hyperfleet-api/plugins/versions"
2426
)
2527

2628
// nolint

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ require (
2020
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103
2121
github.com/oapi-codegen/runtime v1.2.0
2222
github.com/onsi/gomega v1.27.1
23-
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.15
23+
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.18
2424
github.com/prometheus/client_golang v1.16.0
2525
github.com/prometheus/client_model v0.3.0
2626
github.com/spf13/cobra v1.8.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
336336
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
337337
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
338338
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
339-
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.15 h1:hvMiu1kkBEJE4eumMKGfv6Gzl3sTlX5yCW6cEGe8ToM=
340-
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.15/go.mod h1:KITzIAd8HcMpH5lXdHFjgk45dvL6XLpP3wwz8iK+KCI=
339+
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.18 h1:UgSak1CwYPf3glWt4HG1iAoCj03JApaIUzrtGVPG4m4=
340+
github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.18/go.mod h1:KITzIAd8HcMpH5lXdHFjgk45dvL6XLpP3wwz8iK+KCI=
341341
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
342342
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
343343
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=

pkg/api/presenters/resource.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package presenters
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
openapi_types "github.com/oapi-codegen/runtime/types"
8+
"gorm.io/datatypes"
9+
10+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
11+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi"
12+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/util"
13+
)
14+
15+
const listResponseKind = "ResourceList"
16+
17+
// ConvertResource converts an openapi.ResourceCreateRequest to an api.Resource GORM model.
18+
// CreatedBy/UpdatedBy are set by the service layer from auth context.
19+
func ConvertResource(req *openapi.ResourceCreateRequest) (*api.Resource, error) {
20+
specJSON, err := json.Marshal(req.Spec)
21+
if err != nil {
22+
return nil, fmt.Errorf("failed to marshal spec: %w", err)
23+
}
24+
25+
labelsJSON, err := marshalLabels(req.Labels)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to marshal labels: %w", err)
28+
}
29+
30+
return &api.Resource{
31+
Kind: req.Kind,
32+
Name: req.Name,
33+
Spec: specJSON,
34+
Labels: labelsJSON,
35+
}, nil
36+
}
37+
38+
// ConvertResourceWithOwner converts a request to a child resource with owner references set.
39+
func ConvertResourceWithOwner(
40+
req *openapi.ResourceCreateRequest,
41+
ownerID, ownerKind, ownerHref string,
42+
) (*api.Resource, error) {
43+
resource, err := ConvertResource(req)
44+
if err != nil {
45+
return nil, err
46+
}
47+
resource.SetOwner(ownerID, ownerKind, ownerHref)
48+
return resource, nil
49+
}
50+
51+
// PresentResource converts an api.Resource GORM model to an openapi.Resource.
52+
func PresentResource(r *api.Resource) openapi.Resource {
53+
var spec map[string]interface{}
54+
if len(r.Spec) > 0 {
55+
if err := json.Unmarshal(r.Spec, &spec); err != nil {
56+
spec = nil
57+
}
58+
}
59+
60+
labels := unmarshalLabels(r.Labels)
61+
62+
resp := openapi.Resource{
63+
Id: r.ID,
64+
Kind: r.Kind,
65+
Name: r.Name,
66+
Href: util.PtrString(r.Href),
67+
Spec: spec,
68+
Labels: labels,
69+
Generation: r.Generation,
70+
CreatedTime: r.CreatedTime,
71+
UpdatedTime: r.UpdatedTime,
72+
CreatedBy: openapi_types.Email(r.CreatedBy),
73+
UpdatedBy: openapi_types.Email(r.UpdatedBy),
74+
DeletedTime: r.DeletedTime,
75+
}
76+
77+
if r.DeletedBy != nil {
78+
email := openapi_types.Email(*r.DeletedBy)
79+
resp.DeletedBy = &email
80+
}
81+
82+
if r.OwnerID != nil && *r.OwnerID != "" {
83+
resp.OwnerReferences = &struct {
84+
openapi.ObjectReference `yaml:",inline"`
85+
}{
86+
ObjectReference: openapi.ObjectReference{
87+
Id: r.OwnerID,
88+
Kind: r.OwnerKind,
89+
Href: r.OwnerHref,
90+
},
91+
}
92+
}
93+
94+
return resp
95+
}
96+
97+
// PresentResourceList converts a slice of resources and paging metadata to an openapi.ResourceList.
98+
func PresentResourceList(resources api.ResourceList, paging *api.PagingMeta) openapi.ResourceList {
99+
items := make([]openapi.Resource, 0, len(resources))
100+
for i := range resources {
101+
items = append(items, PresentResource(resources[i]))
102+
}
103+
return openapi.ResourceList{
104+
Kind: listResponseKind,
105+
Items: items,
106+
Page: int32(paging.Page),
107+
Size: int32(paging.Size),
108+
Total: paging.Total,
109+
}
110+
}
111+
112+
func marshalLabels(labels *map[string]string) (datatypes.JSON, error) {
113+
if labels == nil {
114+
return datatypes.JSON("{}"), nil
115+
}
116+
b, err := json.Marshal(*labels)
117+
if err != nil {
118+
return nil, err
119+
}
120+
return datatypes.JSON(b), nil
121+
}
122+
123+
func unmarshalLabels(raw datatypes.JSON) *map[string]string {
124+
if len(raw) == 0 {
125+
return nil
126+
}
127+
var m map[string]string
128+
if err := json.Unmarshal(raw, &m); err != nil {
129+
return nil
130+
}
131+
if len(m) == 0 {
132+
return nil
133+
}
134+
return &m
135+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package presenters
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
. "github.com/onsi/gomega"
8+
"gorm.io/datatypes"
9+
10+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
11+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi"
12+
)
13+
14+
func TestConvertResource(t *testing.T) {
15+
RegisterTestingT(t)
16+
17+
labels := map[string]string{"env": "prod"}
18+
req := &openapi.ResourceCreateRequest{
19+
Kind: "Channel",
20+
Name: "stable",
21+
Spec: map[string]any{"is_default": true, "enabled_regex": "4\\.17\\..*"},
22+
Labels: &labels,
23+
}
24+
25+
resource, err := ConvertResource(req)
26+
Expect(err).NotTo(HaveOccurred())
27+
Expect(resource.Kind).To(Equal("Channel"))
28+
Expect(resource.Name).To(Equal("stable"))
29+
Expect(resource.Spec).NotTo(BeEmpty())
30+
Expect(string(resource.Labels)).To(ContainSubstring("env"))
31+
}
32+
33+
func TestConvertResource_NilLabels(t *testing.T) {
34+
RegisterTestingT(t)
35+
36+
req := &openapi.ResourceCreateRequest{
37+
Kind: "Channel",
38+
Name: "test",
39+
Spec: map[string]any{"key": "value"},
40+
}
41+
42+
resource, err := ConvertResource(req)
43+
Expect(err).NotTo(HaveOccurred())
44+
Expect(string(resource.Labels)).To(Equal("{}"))
45+
}
46+
47+
func TestConvertResourceWithOwner(t *testing.T) {
48+
RegisterTestingT(t)
49+
50+
req := &openapi.ResourceCreateRequest{
51+
Kind: "Version",
52+
Name: "4-17-3",
53+
Spec: map[string]any{"raw_version": "4.17.3"},
54+
}
55+
56+
resource, err := ConvertResourceWithOwner(
57+
req,
58+
"parent-id", "Channel", "/api/hyperfleet/v1/channels/parent-id",
59+
)
60+
Expect(err).NotTo(HaveOccurred())
61+
Expect(*resource.OwnerID).To(Equal("parent-id"))
62+
Expect(*resource.OwnerKind).To(Equal("Channel"))
63+
Expect(*resource.OwnerHref).To(Equal("/api/hyperfleet/v1/channels/parent-id"))
64+
}
65+
66+
func TestPresentResource(t *testing.T) {
67+
RegisterTestingT(t)
68+
69+
now := time.Now()
70+
resource := &api.Resource{
71+
Meta: api.Meta{ID: "test-id", CreatedTime: now, UpdatedTime: now},
72+
Kind: "Channel",
73+
Name: "stable",
74+
Href: "/api/hyperfleet/v1/channels/test-id",
75+
Spec: datatypes.JSON(`{"is_default":true}`),
76+
Labels: datatypes.JSON(`{"env":"prod"}`),
77+
Generation: 1,
78+
CreatedBy: "user@test.com",
79+
UpdatedBy: "user@test.com",
80+
}
81+
82+
resp := PresentResource(resource)
83+
Expect(resp.Id).To(Equal("test-id"))
84+
Expect(resp.Kind).To(Equal("Channel"))
85+
Expect(resp.Name).To(Equal("stable"))
86+
Expect(*resp.Href).To(Equal("/api/hyperfleet/v1/channels/test-id"))
87+
Expect(resp.Spec).To(HaveKeyWithValue("is_default", true))
88+
Expect(*resp.Labels).To(HaveKeyWithValue("env", "prod"))
89+
Expect(resp.Generation).To(Equal(int32(1)))
90+
Expect(string(resp.CreatedBy)).To(Equal("user@test.com"))
91+
Expect(resp.OwnerReferences).To(BeNil())
92+
}
93+
94+
func TestPresentResource_WithOwner(t *testing.T) {
95+
RegisterTestingT(t)
96+
97+
now := time.Now()
98+
ownerID := "parent-id"
99+
ownerKind := "Channel"
100+
ownerHref := "/api/hyperfleet/v1/channels/parent-id"
101+
resource := &api.Resource{
102+
Meta: api.Meta{ID: "child-id", CreatedTime: now, UpdatedTime: now},
103+
Kind: "Version",
104+
Name: "4-17-1",
105+
Spec: datatypes.JSON(`{}`),
106+
OwnerID: &ownerID,
107+
OwnerKind: &ownerKind,
108+
OwnerHref: &ownerHref,
109+
CreatedBy: "user@test.com",
110+
UpdatedBy: "user@test.com",
111+
}
112+
113+
resp := PresentResource(resource)
114+
Expect(resp.OwnerReferences).NotTo(BeNil())
115+
Expect(*resp.OwnerReferences.Id).To(Equal("parent-id"))
116+
Expect(*resp.OwnerReferences.Kind).To(Equal("Channel"))
117+
}
118+
119+
func TestPresentResource_EmptySpec(t *testing.T) {
120+
RegisterTestingT(t)
121+
122+
now := time.Now()
123+
resource := &api.Resource{
124+
Meta: api.Meta{ID: "id", CreatedTime: now, UpdatedTime: now},
125+
Kind: "Channel",
126+
Name: "test",
127+
Spec: datatypes.JSON(`{}`),
128+
CreatedBy: "user@test.com",
129+
UpdatedBy: "user@test.com",
130+
}
131+
132+
resp := PresentResource(resource)
133+
Expect(resp.Spec).To(BeEmpty())
134+
}
135+
136+
func TestPresentResourceList(t *testing.T) {
137+
RegisterTestingT(t)
138+
139+
now := time.Now()
140+
resources := api.ResourceList{
141+
&api.Resource{
142+
Meta: api.Meta{ID: "id1", CreatedTime: now, UpdatedTime: now},
143+
Kind: "Channel", Name: "stable",
144+
Spec: datatypes.JSON(`{}`), CreatedBy: "u@t.com", UpdatedBy: "u@t.com",
145+
},
146+
&api.Resource{
147+
Meta: api.Meta{ID: "id2", CreatedTime: now, UpdatedTime: now},
148+
Kind: "Channel", Name: "candidate",
149+
Spec: datatypes.JSON(`{}`), CreatedBy: "u@t.com", UpdatedBy: "u@t.com",
150+
},
151+
}
152+
paging := &api.PagingMeta{Page: 1, Size: 2, Total: 2}
153+
154+
result := PresentResourceList(resources, paging)
155+
Expect(result.Kind).To(Equal("ResourceList"))
156+
Expect(result.Items).To(HaveLen(2))
157+
Expect(result.Page).To(Equal(int32(1)))
158+
Expect(result.Size).To(Equal(int32(2)))
159+
Expect(result.Total).To(Equal(int64(2)))
160+
}

pkg/api/resource.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ func (r *Resource) MarkDeleted(by string, t time.Time) {
9191
r.DeletedBy = &by
9292
}
9393

94+
func (r *Resource) SetOwner(id, kind, href string) {
95+
r.OwnerID = &id
96+
r.OwnerKind = &kind
97+
r.OwnerHref = &href
98+
}
99+
94100
func (r *Resource) IncrementGeneration() {
95101
r.Generation++
96102
}

pkg/api/resource_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ func TestResource_IncrementGeneration(t *testing.T) {
219219
Expect(r.Generation).To(Equal(int32(3)))
220220
}
221221

222+
func TestResource_SetOwner(t *testing.T) {
223+
RegisterTestingT(t)
224+
225+
r := &Resource{Name: "test", Kind: "Version"}
226+
r.SetOwner("parent-123", "Channel", "/api/hyperfleet/v1/channels/parent-123")
227+
228+
Expect(r.OwnerID).ToNot(BeNil())
229+
Expect(*r.OwnerID).To(Equal("parent-123"))
230+
Expect(*r.OwnerKind).To(Equal("Channel"))
231+
Expect(*r.OwnerHref).To(Equal("/api/hyperfleet/v1/channels/parent-123"))
232+
}
233+
222234
func TestResource_TableName(t *testing.T) {
223235
RegisterTestingT(t)
224236

pkg/handlers/cluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func (h ClusterHandler) SoftDelete(w http.ResponseWriter, r *http.Request) {
169169
},
170170
ErrorHandler: handleError,
171171
}
172-
handleSoftDelete(w, r, cfg, http.StatusAccepted)
172+
handleSoftDelete(w, r, cfg)
173173
}
174174

175175
// ForceDelete permanently removes a cluster that is in Finalizing state.

pkg/handlers/cluster_nodepools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func (h ClusterNodePoolsHandler) SoftDelete(w http.ResponseWriter, r *http.Reque
135135
ErrorHandler: handleError,
136136
}
137137

138-
handleSoftDelete(w, r, cfg, http.StatusAccepted)
138+
handleSoftDelete(w, r, cfg)
139139
}
140140

141141
// ForceDelete permanently removes a nodepool that is in Finalizing state.

pkg/handlers/framework.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ func handle(w http.ResponseWriter, r *http.Request, cfg *handlerConfig, httpStat
8585

8686
}
8787

88-
func handleSoftDelete(w http.ResponseWriter, r *http.Request, cfg *handlerConfig, httpStatus int) {
88+
func handleSoftDelete(w http.ResponseWriter, r *http.Request, cfg *handlerConfig) {
89+
httpStatus := http.StatusAccepted
8990
if cfg.ErrorHandler == nil {
9091
cfg.ErrorHandler = handleError
9192
}

0 commit comments

Comments
 (0)