Skip to content

Commit 26d7d13

Browse files
authored
Update Go version and dependencies; add test cases for merging duplic… (#6)
* Update Go version and dependencies; add test cases for merging duplicated contexts, clusters, and users * Update GitHub Actions workflows to use latest action versions * formatted * Refactor validation functions to export them and add unit tests for parsing and validation * Fix missing newline at end of file in main_test.go * Update README and main.go for improved CLI tool description and argument handling
1 parent 043954f commit 26d7d13

13 files changed

+261
-129
lines changed

.github/workflows/ci.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111
steps:
1212

1313
- name: Checkout
14-
uses: actions/checkout@v3
14+
uses: actions/checkout@v4
1515

1616
- name: Setup Go
17-
uses: actions/setup-go@v3
17+
uses: actions/setup-go@v5
1818
with:
19-
go-version: 1.19
19+
go-version: 1.23.6
2020
cache: true
2121
cache-dependency-path: go.sum
2222

@@ -30,7 +30,7 @@ jobs:
3030
run: go mod tidy && git diff --no-patch --exit-code
3131

3232
- name: Build with Goreleaser
33-
uses: goreleaser/goreleaser-action@v2
33+
uses: goreleaser/goreleaser-action@v6
3434
with:
3535
version: latest
3636
args: release --snapshot --clean
@@ -60,7 +60,7 @@ jobs:
6060
thresholds: '50 75'
6161

6262
- name: Upload Go test results
63-
uses: actions/upload-artifact@v3
63+
uses: actions/upload-artifact@v4
6464
with:
6565
name: go-test-coverage-report
6666
path: coverage.html

.github/workflows/release.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ jobs:
1111
steps:
1212

1313
- name: Checkout
14-
uses: actions/checkout@v3
14+
uses: actions/checkout@v4
1515
with:
1616
fetch-depth: 0
1717

1818
- run: git fetch --force --tags
1919

2020
- name: Setup Go
21-
uses: actions/setup-go@v3
21+
uses: actions/setup-go@v5
2222
with:
23-
go-version: 1.19
23+
go-version: 1.23.6
2424
cache: true
2525
cache-dependency-path: go.sum
2626

2727
- name: GoReleaser
28-
uses: goreleaser/goreleaser-action@v4
28+
uses: goreleaser/goreleaser-action@v6
2929
with:
3030
version: latest
3131
args: release --clean

.goreleaser.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ changelog:
2121
filters:
2222
exclude:
2323
- '^docs:'
24-
- '^test:'
24+
- '^test:'

README.md

+20-23
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
![build](https://img.shields.io/github/actions/workflow/status/btungut/kubeconfig-merge/ci.yml?branch=master)
66
![go](https://img.shields.io/github/go-mod/go-version/btungut/kubeconfig-merge)
77

8-
## Why is it developed?
9-
If you are someone who works with a significant number of Kubernetes clusters, dealing with `kubecontext` in a manual way can be **boring** and also **result in problems**.
10-
In addition to that, I am currently working with more than 20 customers, which results in an average of five clusters per customer.
8+
# 🌟 kubeconfig-merge
119

12-
## Install
10+
`kubeconfig-merge` is a lightweight and efficient CLI tool designed to **merge multiple Kubernetes `kubeconfig` files** into a single, well-structured configuration. It ensures that the resulting configuration is clean, free of conflicts, and compatible with `kubectl` and other Kubernetes clients.
11+
12+
## 🚀 Features
13+
14+
**Merge multiple `kubeconfig` files** into one unified configuration
15+
**Preserve existing contexts, clusters, and users** without conflicts
16+
**Ensure a clean and well-structured config file**
17+
**Works seamlessly with `kubectl` and Kubernetes clients**
18+
**Lightweight, fast, and easy to use**
19+
20+
## 📌 Installation
1321

1422
### Install on Linux
1523

@@ -25,7 +33,6 @@ The following instruction list covers all of the Linux distributions (Ubuntu, De
2533
FILENAME="kubeconfig-merge_${OS}_${ARCH}" &&
2634
curl -fsSLO "https://github.com/btungut/kubeconfig-merge/releases/latest/download/${FILENAME}" &&
2735
sudo rm -rf "$EXEC_PATH" && sudo cp "${FILENAME}" "$EXEC_PATH" && sudo chmod +x "$EXEC_PATH"
28-
)
2936
```
3037
3138
### Install on Windows
@@ -34,26 +41,16 @@ TBD
3441
3542
## Arguments
3643
37-
| Argument | Description | Default |
38-
|--------------|----------------------------------------------------------------------------|------------------------------------------------|
39-
| file | The additional kubeconfig file | *Required* |
40-
| kubeconfig | The kubeconfig file which to be append into | `KUBECONFIG` env variable, or `~/.kube/config` |
41-
| name | Context, cluster and user name of new entries | File name of `--file`|
44+
| Argument | Type | Description | Default |
45+
| ---------- | ------- | ----------------------------------------------- | ---------------------------------------------- |
46+
| kubeconfig | string | The kubeconfig file which to be append into | `KUBECONFIG` env variable, or `~/.kube/config` |
47+
| file | string | To be appended kubeconfig file | **Required** |
48+
| override | boolean | Use file name for the cluster, context and user | Optional |
4249
43-
## Examples
4450
45-
---
46-
### `./kubeconfig-merge --file valid-default-cluster.yaml`
47-
48-
![kubeconfig-merge without name](.assets/kubeconfig-merge-01.png)
51+
## Examples
4952
50-
<br/>
5153
52-
---
53-
54-
### `./kubeconfig-merge --file valid-default-cluster.yaml --name foo`
55-
![kubeconfig-merge with name](.assets/kubeconfig-merge-02.png)
54+
### `./kubeconfig-merge --file valid-default-cluster.yaml`
5655
57-
## Contributors
58-
[Ohki Nozomu](https://github.com/ohkinozomu)
59-
56+
![kubeconfig-merge without name](.assets/kubeconfig-merge-01.png)

go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module github.com/btungut/kubeconfig-merge
22

3-
go 1.19
3+
go 1.23.6
44

55
require (
6-
github.com/google/uuid v1.3.0
7-
github.com/stretchr/testify v1.8.1
6+
github.com/google/uuid v1.6.0
7+
github.com/stretchr/testify v1.10.0
88
gopkg.in/yaml.v3 v3.0.1
99
)
1010

go.sum

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
32
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4-
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
5-
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
65
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
76
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
10-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
11-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
12-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
13-
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
14-
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
159
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1610
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1811
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1912
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

+80-26
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@ type KubectlClusterWithName struct {
5656

5757
const KUBECONFIG_ENV = "KUBECONFIG"
5858
const KUBECONFIG_ENV_KEY = "$KUBECONFIG"
59-
const KUBECONFIG_DEFAULT_PATH = "~/.kube/config"
59+
60+
var KUBECONFIG_DEFAULT_PATH = func() string {
61+
home, err := os.UserHomeDir()
62+
if err != nil {
63+
log.Panic(err)
64+
}
65+
return filepath.Join(home, ".kube", "config")
66+
}()
6067

6168
func ParseKubeConfig(path string) (*KubectlConfig, error) {
6269

@@ -74,7 +81,7 @@ func ParseKubeConfig(path string) (*KubectlConfig, error) {
7481
return &kubeConfig, nil
7582
}
7683

77-
func validate(toBeAppend KubectlConfig, kubectlConfig KubectlConfig, name string) error {
84+
func ValidateOnlyOneContext(toBeAppend KubectlConfig, kubectlConfig KubectlConfig) error {
7885

7986
if len(toBeAppend.Clusters) != 1 {
8087
return errors.New("only one cluster can be merged into original kubeconfig")
@@ -88,49 +95,91 @@ func validate(toBeAppend KubectlConfig, kubectlConfig KubectlConfig, name string
8895
return errors.New("only one context can be merged into original kubeconfig")
8996
}
9097

91-
for _, v := range kubectlConfig.Clusters {
92-
if strings.EqualFold(v.Name, name) {
93-
return fmt.Errorf("a cluster entry with %s already exists in kubeconfig, merge failed", name)
98+
return nil
99+
}
100+
101+
func ValidateDuplication(toBeAppend KubectlConfig, kubectlConfig KubectlConfig) error {
102+
103+
for _, v1 := range kubectlConfig.Clusters {
104+
for _, v2 := range toBeAppend.Clusters {
105+
if strings.EqualFold(v1.Name, v2.Name) {
106+
return fmt.Errorf("a cluster entry with %s already exists in kubeconfig, merge failed", v1.Name)
107+
}
94108
}
95109
}
96110

97-
for _, v := range kubectlConfig.Contexts {
98-
if strings.EqualFold(v.Name, name) {
99-
return fmt.Errorf("a context entry with %s already exists in kubeconfig, merge failed", name)
111+
for _, v1 := range kubectlConfig.Users {
112+
for _, v2 := range toBeAppend.Users {
113+
if strings.EqualFold(v1.Name, v2.Name) {
114+
return fmt.Errorf("a user entry with %s already exists in kubeconfig, merge failed", v1.Name)
115+
}
100116
}
101117
}
102118

103-
for _, v := range kubectlConfig.Users {
104-
if strings.EqualFold(v.Name, name) {
105-
return fmt.Errorf("a user entry with %s already exists in kubeconfig, merge failed", name)
119+
for _, v1 := range kubectlConfig.Contexts {
120+
for _, v2 := range toBeAppend.Contexts {
121+
if strings.EqualFold(v1.Name, v2.Name) {
122+
return fmt.Errorf("a context entry with %s already exists in kubeconfig, merge failed", v1.Name)
123+
}
106124
}
107125
}
108126

109127
return nil
110128
}
111129

112-
func Merge(kubeConfig KubectlConfig, toBeAppend KubectlConfig, name, toBeAppendFileName string) (*KubectlConfig, error) {
130+
func Merge(kubeConfig KubectlConfig, toBeAppend KubectlConfig, name string, override bool) (*KubectlConfig, error) {
113131

114-
var newName = toBeAppendFileName
115-
if len(name) > 0 {
116-
newName = name
132+
var err = ValidateOnlyOneContext(toBeAppend, kubeConfig)
133+
if err != nil {
134+
return nil, err
117135
}
118136

119-
var err = validate(toBeAppend, kubeConfig, toBeAppendFileName)
137+
toBeAppend.Clusters[0].Name = name
138+
toBeAppend.Users[0].Name = name
139+
toBeAppend.Contexts[0].Name = name
140+
toBeAppend.Contexts[0].Context.Cluster = name
141+
toBeAppend.Contexts[0].Context.User = name
142+
143+
var needOverride = false
144+
err = ValidateDuplication(toBeAppend, kubeConfig)
120145
if err != nil {
121-
return nil, err
146+
if !override {
147+
return nil, err
148+
}
149+
needOverride = true
150+
}
151+
152+
if needOverride {
153+
// remove the existing cluster, user and context
154+
for i, v := range kubeConfig.Clusters {
155+
if strings.EqualFold(v.Name, name) {
156+
kubeConfig.Clusters = append(kubeConfig.Clusters[:i], kubeConfig.Clusters[i+1:]...)
157+
break
158+
}
159+
}
160+
161+
for i, v := range kubeConfig.Users {
162+
if strings.EqualFold(v.Name, name) {
163+
kubeConfig.Users = append(kubeConfig.Users[:i], kubeConfig.Users[i+1:]...)
164+
break
165+
}
166+
}
167+
168+
for i, v := range kubeConfig.Contexts {
169+
if strings.EqualFold(v.Name, name) {
170+
kubeConfig.Contexts = append(kubeConfig.Contexts[:i], kubeConfig.Contexts[i+1:]...)
171+
break
172+
}
173+
}
174+
175+
fmt.Printf("Cluster, context and user with '%s' name is removed because of override flag\n", name)
122176
}
123177

124-
toBeAppend.Clusters[0].Name = newName
125-
toBeAppend.Users[0].Name = newName
126-
toBeAppend.Contexts[0].Name = newName
127-
toBeAppend.Contexts[0].Context.Cluster = newName
128-
toBeAppend.Contexts[0].Context.User = newName
129178
kubeConfig.Clusters = append(kubeConfig.Clusters, toBeAppend.Clusters[0])
130179
kubeConfig.Users = append(kubeConfig.Users, toBeAppend.Users[0])
131180
kubeConfig.Contexts = append(kubeConfig.Contexts, toBeAppend.Contexts[0])
132181

133-
fmt.Printf("Cluster, context and user will be added with '%s' name\n", newName)
182+
fmt.Printf("Cluster, context and user will be added with '%s' name\n", name)
134183

135184
return &kubeConfig, nil
136185
}
@@ -156,7 +205,7 @@ func getKubeConfigPath(passedValue string) string {
156205
func main() {
157206
kubeConfigPtr := flag.String("kubeconfig", "", fmt.Sprintf("path to the kubeconfig file (defaults '%s' or '%s')", KUBECONFIG_ENV_KEY, KUBECONFIG_DEFAULT_PATH))
158207
filePtr := flag.String("file", "", "path to the yaml file that to be append into kubeconfig")
159-
namePtr := flag.String("name", "", "Replaces the name of context, user and cluster (default file name of --file argument)")
208+
overridePtr := flag.Bool("override", false, "Override the existing context, user and cluster with the file name, or the fields in the file will be used")
160209
flag.Parse()
161210

162211
var kubeConfigPath = getKubeConfigPath(*kubeConfigPtr)
@@ -165,14 +214,19 @@ func main() {
165214
if err != nil {
166215
log.Panic(err)
167216
}
217+
if filepath.Ext(*filePtr) == "" {
218+
log.Panic("the file specified by --file must have a valid extension")
219+
}
168220

169221
toBeAppend, err := ParseKubeConfig(*filePtr)
170222
if err != nil {
171223
log.Panic(err)
172224
}
173225

174226
var fileName = filepath.Base(*filePtr)
175-
result, err := Merge(*kubeConfig, *toBeAppend, *namePtr, fileName[:len(fileName)-len(filepath.Ext(fileName))])
227+
fileName = strings.TrimSuffix(fileName, filepath.Ext(fileName))
228+
fileName = strings.ToLower(fileName)
229+
result, err := Merge(*kubeConfig, *toBeAppend, fileName, *overridePtr)
176230
if err != nil {
177231
log.Panic(err)
178232
}
@@ -186,5 +240,5 @@ func main() {
186240
if err != nil {
187241
log.Panic(err)
188242
}
189-
fmt.Printf("%s was modified successfully\n", kubeConfigPath)
243+
log.Printf("%s was modified successfully\n", kubeConfigPath)
190244
}

0 commit comments

Comments
 (0)