Skip to content

Commit fde043c

Browse files
author
柏存
committed
feat: rbgctl supports rbg revision operations
Signed-off-by: 柏存 <guoxiongfeng.gxf@alibaba-inc.com>
1 parent 3ce572a commit fde043c

File tree

695 files changed

+110703
-262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

695 files changed

+110703
-262
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ build-cli: ## Build cli binary.
145145
CGO_ENABLED=0 \
146146
GO111MODULE=on \
147147
GOPROXY=${GOPROXY} \
148-
go build -mod vendor -v -o bin/kubectl-rbg-status -ldflags $(ldflags) cmd/cli/main.go
148+
go build -mod vendor -v -o bin/kubectl-rbg -ldflags $(ldflags) cmd/cli/main.go
149149

150150
.PHONY: run
151151
run: manifests generate fmt vet ## Run a controller from your host.

cmd/cli/cmd/rollout/rollout.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package rollout
18+
19+
import (
20+
"sort"
21+
22+
"github.com/spf13/cobra"
23+
appsv1 "k8s.io/api/apps/v1"
24+
"k8s.io/cli-runtime/pkg/genericclioptions"
25+
)
26+
27+
type RolloutOptions struct {
28+
cf *genericclioptions.ConfigFlags
29+
revision int64
30+
}
31+
32+
var rolloutOpts RolloutOptions
33+
34+
func NewRolloutCmd(cf *genericclioptions.ConfigFlags) *cobra.Command {
35+
rolloutOpts.cf = cf
36+
rolloutCmd := &cobra.Command{
37+
Use: "rollout [SUBCOMMAND]",
38+
Short: "Manage the rollout of a rbg object",
39+
Example: " # Show all historical revisions of rbg\n" +
40+
" kubectl rbg rollout history abc\n" +
41+
" # Rollback to the previous deployment\n" +
42+
" kubectl rbg rollout undo abc\n",
43+
Args: cobra.ExactArgs(1),
44+
DisableAutoGenTag: true,
45+
SilenceUsage: true,
46+
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
47+
}
48+
49+
rolloutCmd.AddCommand(rolloutHistoryCmd)
50+
rolloutCmd.AddCommand(rolloutDiffCmd)
51+
rolloutCmd.AddCommand(rolloutUndoCmd)
52+
return rolloutCmd
53+
}
54+
55+
func sortRevisionsStable(items []*appsv1.ControllerRevision) []*appsv1.ControllerRevision {
56+
sort.SliceStable(items, func(i, j int) bool {
57+
if items[i].Revision == items[j].Revision {
58+
if items[i].CreationTimestamp.Equal(&items[j].CreationTimestamp) {
59+
return items[i].Name < items[j].Name
60+
}
61+
return items[i].CreationTimestamp.Before(&items[j].CreationTimestamp)
62+
}
63+
return items[i].Revision < items[j].Revision
64+
})
65+
return items
66+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package rollout
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/google/go-cmp/cmp"
24+
"github.com/spf13/cobra"
25+
"go.yaml.in/yaml/v2"
26+
appsv1 "k8s.io/api/apps/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/client-go/kubernetes"
29+
workloadsv1alpha1 "sigs.k8s.io/rbgs/api/workloads/v1alpha1"
30+
"sigs.k8s.io/rbgs/client-go/clientset/versioned"
31+
"sigs.k8s.io/rbgs/cmd/cli/util"
32+
)
33+
34+
var rolloutDiffCmd = &cobra.Command{
35+
Use: "diff <rbgName>",
36+
Short: "Show the diff between the current rbg and the specified revision",
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
if err := validateRolloutDiff(args); err != nil {
39+
return err
40+
}
41+
rbgClient, err := util.GetRBGClient(rolloutOpts.cf)
42+
if err != nil {
43+
return err
44+
}
45+
k8sClient, err := util.GetK8SClientSet(rolloutOpts.cf)
46+
if err != nil {
47+
return err
48+
}
49+
return runRolloutDiff(context.Background(), rbgClient, k8sClient, args[0], util.GetNamespace(rolloutOpts.cf))
50+
},
51+
}
52+
53+
func init() {
54+
rolloutDiffCmd.Flags().Int64Var(&rolloutOpts.revision, "revision", rolloutOpts.revision, "specific revision to compare")
55+
}
56+
57+
func validateRolloutDiff(args []string) error {
58+
if len(args) == 0 || len(args[0]) == 0 {
59+
return fmt.Errorf("rbg name is required")
60+
}
61+
if rolloutOpts.revision <= 0 {
62+
return fmt.Errorf("--revision must be positive")
63+
}
64+
return nil
65+
}
66+
67+
func runRolloutDiff(ctx context.Context, rbgClient versioned.Interface, k8sClient kubernetes.Interface, rbgName, namespace string) error {
68+
rbgObject, err := rbgClient.WorkloadsV1alpha1().RoleBasedGroups(namespace).Get(ctx, rbgName, metav1.GetOptions{})
69+
if err != nil {
70+
return err
71+
}
72+
if rbgObject == nil {
73+
return fmt.Errorf("RoleBasedGroup %s not found", rbgName)
74+
}
75+
76+
// List ControllerRevision
77+
revisions, err := k8sClient.AppsV1().
78+
ControllerRevisions(namespace).
79+
List(context.TODO(), metav1.ListOptions{
80+
LabelSelector: fmt.Sprintf("%s=%s", workloadsv1alpha1.SetNameLabelKey, rbgObject.Name),
81+
})
82+
if err != nil {
83+
return err
84+
}
85+
86+
history := revisions.Items
87+
var items []*appsv1.ControllerRevision
88+
for i := range history {
89+
ref := metav1.GetControllerOfNoCopy(&history[i])
90+
if ref == nil || ref.UID == rbgObject.GetUID() {
91+
items = append(items, &history[i])
92+
}
93+
94+
}
95+
var currentRevision *appsv1.ControllerRevision
96+
var specificRevision *appsv1.ControllerRevision
97+
for _, rev := range items {
98+
if rev.Revision == rolloutOpts.revision {
99+
specificRevision = rev
100+
}
101+
if currentRevision == nil || currentRevision.Revision <= rev.Revision {
102+
currentRevision = rev
103+
}
104+
}
105+
106+
if specificRevision == nil {
107+
return fmt.Errorf("--revision=%d not found, please check the revision number", rolloutOpts.revision)
108+
} else if currentRevision == nil {
109+
return fmt.Errorf("current revision not found, please try again later")
110+
}
111+
112+
res, err := extractDiff(currentRevision, specificRevision)
113+
if err != nil {
114+
return err
115+
}
116+
117+
fmt.Println(res)
118+
return nil
119+
}
120+
121+
func extractDiff(currentRevision, specificRevision *appsv1.ControllerRevision) (string, error) {
122+
spec1, err := extractSpec(currentRevision)
123+
if err != nil {
124+
return "", err
125+
}
126+
spec2, err := extractSpec(specificRevision)
127+
if err != nil {
128+
return "", err
129+
}
130+
131+
spec1Yaml, err := yaml.Marshal(spec1)
132+
if err != nil {
133+
return "", err
134+
}
135+
spec2Yaml, err := yaml.Marshal(spec2)
136+
if err != nil {
137+
return "", err
138+
}
139+
140+
return cmp.Diff(string(spec2Yaml), string(spec1Yaml)), nil
141+
}
142+
143+
func extractSpec(rev *appsv1.ControllerRevision) (interface{}, error) {
144+
if len(rev.Data.Raw) == 0 {
145+
return nil, fmt.Errorf("controller revision has no raw data")
146+
}
147+
var raw map[string]interface{}
148+
if err := yaml.Unmarshal(rev.Data.Raw, &raw); err != nil {
149+
return nil, err
150+
}
151+
return raw["spec"], nil
152+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package rollout
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
appsv1 "k8s.io/api/apps/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
workloadsv1alpha1 "sigs.k8s.io/rbgs/api/workloads/v1alpha1"
29+
)
30+
31+
func TestValidateRolloutDiff(t *testing.T) {
32+
tests := []struct {
33+
name string
34+
args []string
35+
revision int64
36+
expectError bool
37+
}{
38+
{
39+
name: "valid args with name and positive revision",
40+
args: []string{"test-rbg"},
41+
revision: 1,
42+
expectError: false,
43+
},
44+
{
45+
name: "empty args",
46+
args: []string{},
47+
revision: 1,
48+
expectError: true,
49+
},
50+
{
51+
name: "empty name",
52+
args: []string{""},
53+
revision: 1,
54+
expectError: true,
55+
},
56+
{
57+
name: "zero revision",
58+
args: []string{"test-rbg"},
59+
revision: 0,
60+
expectError: true,
61+
},
62+
{
63+
name: "negative revision",
64+
args: []string{"test-rbg"},
65+
revision: -1,
66+
expectError: true,
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
// 设置全局变量
73+
rolloutOpts.revision = tt.revision
74+
err := validateRolloutDiff(tt.args)
75+
76+
if tt.expectError {
77+
assert.Error(t, err)
78+
} else {
79+
assert.NoError(t, err)
80+
}
81+
})
82+
}
83+
}
84+
85+
func TestRunRolloutDiff(t *testing.T) {
86+
rbg := &workloadsv1alpha1.RoleBasedGroup{
87+
ObjectMeta: metav1.ObjectMeta{
88+
Name: "test-rbg",
89+
Namespace: "default",
90+
UID: "12345",
91+
},
92+
}
93+
var revisions []*appsv1.ControllerRevision
94+
for i := 0; i < 5; i++ {
95+
revisions = append(revisions, &appsv1.ControllerRevision{
96+
ObjectMeta: metav1.ObjectMeta{
97+
Name: fmt.Sprintf("rev-%d", i),
98+
Namespace: "default",
99+
CreationTimestamp: metav1.Now(),
100+
Labels: map[string]string{
101+
workloadsv1alpha1.SetNameLabelKey: "test-rbg",
102+
},
103+
},
104+
Data: runtime.RawExtension{Raw: []byte(fmt.Sprintf("{\"role\":{\"name\":\"role-%d\"}}", i))},
105+
Revision: int64(i),
106+
})
107+
}
108+
fakeClient := getFakeK8sClient(revisions)
109+
fakeRgbClient := getFakeRgbClient([]*workloadsv1alpha1.RoleBasedGroup{rbg})
110+
old := rolloutOpts
111+
defer func() {
112+
rolloutOpts = old
113+
}()
114+
rolloutOpts.revision = 1
115+
err := runRolloutDiff(context.TODO(), fakeRgbClient, fakeClient, "test-rbg", "default")
116+
assert.NoError(t, err)
117+
}

0 commit comments

Comments
 (0)