mirror of https://github.com/fluxcd/flux2.git
Compare commits
250 Commits
Author | SHA1 | Date |
---|---|---|
|
9b944da896 | 6 hours ago |
|
5b37a6b04b | 11 hours ago |
|
9f18062d43 | 4 days ago |
|
1055f28524 | 5 days ago |
|
7b0021c1a8 | 5 days ago |
|
ba997449aa | 5 days ago |
|
ca2f0205c4 | 5 days ago |
|
058525fe37 | 5 days ago |
|
686ee31f8a | 5 days ago |
|
767f235f94 | 5 days ago |
|
d5a2c66746 | 6 days ago |
|
f2ff083b8e | 6 days ago |
|
8c45f25f33 | 6 days ago |
|
f85cbfa9c8 | 1 week ago |
|
71a3dad213 | 1 week ago |
|
72e0535958 | 1 week ago |
|
4f2d1c3a2a | 1 week ago |
|
8e99cf7c93 | 1 week ago |
|
2bb7f38603 | 1 week ago |
|
0fe4449870 | 1 week ago |
|
7c5fb2297c | 1 week ago |
|
f4a811fbd3 | 1 week ago |
|
bb3726bb87 | 1 week ago |
|
333c8fe704 | 1 week ago |
|
83213ce83f | 1 week ago |
|
69718599ac | 1 week ago |
|
0255957dd7 | 2 weeks ago |
|
69b4b85cd9 | 2 weeks ago |
|
a9b5be7ff4 | 2 weeks ago |
|
1b46056e7d | 2 weeks ago |
|
039d79b3c2 | 2 weeks ago |
|
66b8aca399 | 2 weeks ago |
|
41c413e178 | 2 weeks ago |
|
d5f8720c4d | 2 weeks ago |
|
e6eb9d79e3 | 2 weeks ago |
|
b90d1738a9 | 2 weeks ago |
|
f9e66dee9e | 2 weeks ago |
|
f251e8e8a9 | 2 weeks ago |
|
44f0d50dbf | 2 weeks ago |
|
4664d49e29 | 2 weeks ago |
|
2997645ea3 | 2 weeks ago |
|
3247a46654 | 3 weeks ago |
|
b5ecb9bc56 | 3 weeks ago |
|
550260638d | 3 weeks ago |
|
b52d76d6e6 | 3 weeks ago |
|
95b2d855cb | 3 weeks ago |
|
52e0c9815b | 3 weeks ago |
|
154069893b | 3 weeks ago |
|
6185366b8a | 3 weeks ago |
|
f7665f4b47 | 3 weeks ago |
|
b20eb0ca22 | 3 weeks ago |
|
8000a41015 | 3 weeks ago |
|
4601a304dd | 3 weeks ago |
|
2fc09963e8 | 3 weeks ago |
|
a2b4edc2f3 | 3 weeks ago |
|
55bb3fe643 | 3 weeks ago |
|
7060770258 | 3 weeks ago |
|
c3eadad983 | 3 weeks ago |
|
e56dfcacf2 | 3 weeks ago |
|
56e73ae03c | 3 weeks ago |
|
7a2f77ffe0 | 3 weeks ago |
|
c1b2c7cae8 | 3 weeks ago |
|
79186a0055 | 3 weeks ago |
|
e7f1faea01 | 3 weeks ago |
|
74edb12bd1 | 3 weeks ago |
|
48d509d838 | 3 weeks ago |
|
948ed45f10 | 3 weeks ago |
|
6f47ae0f2f | 3 weeks ago |
|
a1f366933b | 4 weeks ago |
|
99b51ad525 | 4 weeks ago |
|
b6e0e8fd63 | 4 weeks ago |
|
9056ec029c | 4 weeks ago |
|
9caea521ea | 4 weeks ago |
|
a317f7c445 | 4 weeks ago |
|
698a68424f | 4 weeks ago |
|
5556a5cc9a | 4 weeks ago |
|
c416671ec4 | 4 weeks ago |
|
f719d2bf76 | 4 weeks ago |
|
46aa068fda | 4 weeks ago |
|
3542d61afd | 4 weeks ago |
|
0a87ed5a42 | 4 weeks ago |
|
e4dcc4bd5f | 4 weeks ago |
|
b4bc0d4932 | 4 weeks ago |
|
6cc446af00 | 4 weeks ago |
|
8db628cc90 | 4 weeks ago |
|
e765897df7 | 4 weeks ago |
|
210b2aa458 | 4 weeks ago |
|
a8e0ea495d | 1 month ago |
|
8fb1ccebfa | 1 month ago |
|
664423230d | 1 month ago |
|
0c8cfcdc85 | 1 month ago |
|
89d4467a50 | 1 month ago |
|
bef6f36755 | 1 month ago |
|
6125991b78 | 1 month ago |
|
64bfa02db4 | 1 month ago |
|
1e662e5ed9 | 1 month ago |
|
df57392f48 | 1 month ago |
|
19cd02e548 | 1 month ago |
|
8bc7822fe5 | 1 month ago |
|
e97da26435 | 1 month ago |
|
1a89fa419e | 1 month ago |
|
7c0e70b9cc | 1 month ago |
|
ed9ee95dbe | 1 month ago |
|
63a38ab228 | 1 month ago |
|
c2a883e25a | 1 month ago |
|
24ae50cfd5 | 1 month ago |
|
0573138e38 | 1 month ago |
|
2f14313646 | 1 month ago |
|
e135336aae | 1 month ago |
|
64eeda58e6 | 1 month ago |
|
acdf523c54 | 1 month ago |
|
e2abf8e358 | 1 month ago |
|
d340f80d75 | 1 month ago |
|
76d36cb429 | 2 months ago |
|
a7fadcd344 | 2 months ago |
|
f19f8611f4 | 2 months ago |
|
8cccb90f90 | 2 months ago |
|
1408bb8294 | 2 months ago |
|
45837d2d1b | 2 months ago |
|
ccb9d12927 | 3 months ago |
|
8b95a09319 | 3 months ago |
|
8176d88801 | 3 months ago |
|
2f850743fa | 3 months ago |
|
4e53b6cb8d | 3 months ago |
|
0bb2e3929f | 3 months ago |
|
82b38dfa68 | 3 months ago |
|
b3b404ed30 | 3 months ago |
|
45990633e6 | 3 months ago |
|
97937c55bf | 3 months ago |
|
f79c44ee0a | 3 months ago |
|
16eb212609 | 3 months ago |
|
5da5186b3b | 3 months ago |
|
158618e632 | 3 months ago |
|
81bd619abd | 3 months ago |
|
d2aa9fb996 | 3 months ago |
|
315dad8682 | 3 months ago |
|
600ec37524 | 3 months ago |
|
1af7e08f07 | 3 months ago |
|
61a19cac84 | 3 months ago |
|
fa8ef5b9d1 | 3 months ago |
|
eb5904fb9d | 4 months ago |
|
fda72a014c | 4 months ago |
|
f4d6934a6f | 4 months ago |
|
545b338004 | 4 months ago |
|
a8425f50bd | 4 months ago |
|
24bf751d4d | 4 months ago |
|
cf157ad8a3 | 4 months ago |
|
5a4bc9410b | 4 months ago |
|
de594183bd | 4 months ago |
|
4c343893c5 | 4 months ago |
|
8ae0aaa46c | 4 months ago |
|
6b3a1134bd | 4 months ago |
|
40a9b495b2 | 4 months ago |
|
1d34e5355b | 4 months ago |
|
00d0e1af25 | 4 months ago |
|
9f29702f54 | 4 months ago |
|
7626cd0c86 | 4 months ago |
|
5291902fd7 | 4 months ago |
|
1757d964c0 | 4 months ago |
|
999f61c02e | 4 months ago |
|
5eb43e4566 | 4 months ago |
|
ec3804cc6f | 4 months ago |
|
4c3aed9faf | 4 months ago |
|
06e3047a2f | 4 months ago |
|
99e6791f4b | 4 months ago |
|
9cad95dda5 | 4 months ago |
|
76c584e751 | 4 months ago |
|
cd4244ae65 | 4 months ago |
|
1d6137d39d | 4 months ago |
|
be8acc0cfb | 4 months ago |
|
2f5f40d593 | 4 months ago |
|
4172a8a7f9 | 4 months ago |
|
4addf8a528 | 4 months ago |
|
1df7697811 | 4 months ago |
|
4c66d37545 | 5 months ago |
|
481c3c6e1e | 5 months ago |
|
1d1d96b489 | 5 months ago |
|
0b972771fd | 5 months ago |
|
650732109e | 5 months ago |
|
79fed691ca | 5 months ago |
|
b37ba736fa | 5 months ago |
|
65766ff4fc | 5 months ago |
|
19d9b87c62 | 5 months ago |
|
d82ec5a211 | 5 months ago |
|
5e5ffdbcc3 | 5 months ago |
|
13ec11da58 | 5 months ago |
|
2948d5e70f | 5 months ago |
|
bb9a119456 | 5 months ago |
|
22ac16f3a1 | 5 months ago |
|
79a654d605 | 5 months ago |
|
a421ce4266 | 5 months ago |
|
4b42c9e746 | 5 months ago |
|
5ffca1b157 | 5 months ago |
|
0951061b5e | 5 months ago |
|
ad9d63ac52 | 5 months ago |
|
dccc658273 | 5 months ago |
|
ef5389cd56 | 5 months ago |
|
3f63b3e864 | 5 months ago |
|
656d9d892d | 5 months ago |
|
e979df122a | 5 months ago |
|
8804b856ea | 5 months ago |
|
3ba170e4d4 | 5 months ago |
|
6150fe9942 | 5 months ago |
|
3e80c5809e | 5 months ago |
|
8928ac7d39 | 5 months ago |
|
289645f142 | 5 months ago |
|
8d9cbe7693 | 5 months ago |
|
392a33d425 | 5 months ago |
|
6af448e037 | 5 months ago |
|
ac66adc24c | 5 months ago |
|
0a64800784 | 5 months ago |
|
941af6a648 | 5 months ago |
|
a6b5013649 | 6 months ago |
|
c18ab38877 | 6 months ago |
|
8738712cfa | 6 months ago |
|
8816c5f7de | 6 months ago |
|
e9e15c5f7a | 6 months ago |
|
5b582917ec | 6 months ago |
|
d9b66f6959 | 6 months ago |
|
1b98e16940 | 6 months ago |
|
0c73420ccf | 6 months ago |
|
8cb7188919 | 6 months ago |
|
72a2866508 | 6 months ago |
|
912718103c | 6 months ago |
|
a7e41df1e3 | 6 months ago |
|
c436708a13 | 6 months ago |
|
3f4743037b | 6 months ago |
|
7b551b0d35 | 6 months ago |
|
bb8a10bab8 | 6 months ago |
|
09af0becc5 | 6 months ago |
|
d84bff7d1b | 6 months ago |
|
a4c513487e | 6 months ago |
|
2046003714 | 6 months ago |
|
f07ee355ea | 6 months ago |
|
5e02724e49 | 6 months ago |
|
e5926bcaad | 6 months ago |
|
355f2bc5f3 | 6 months ago |
|
7e8e0ab772 | 6 months ago |
|
f0fecf7399 | 7 months ago |
|
54db4ffc8b | 7 months ago |
|
73fff7404f | 7 months ago |
|
24057743bb | 7 months ago |
|
04d87be082 | 8 months ago |
|
e7c6ebccc3 | 8 months ago |
|
48382f885b | 8 months ago |
|
511d8346f2 | 8 months ago |
|
f0e8e84ee0 | 8 months ago |
|
c277fbf14e | 8 months ago |
|
28570296a9 | 8 months ago |
|
39ec0cb594 | 8 months ago |
@ -1,34 +1,13 @@
|
|||||||
name: backport
|
name: backport
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [closed, labeled]
|
types: [closed, labeled]
|
||||||
|
permissions: read-all
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pull-request:
|
backport:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write # for reading and creating branches.
|
||||||
pull-requests: write
|
pull-requests: write # for creating pull requests against release branches.
|
||||||
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
|
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.4.0
|
||||||
steps:
|
secrets:
|
||||||
- name: Checkout
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
- name: Create backport PRs
|
|
||||||
uses: korthout/backport-action@be567af183754f6a5d831ae90f648954763f17f5 # v3.1.0
|
|
||||||
# xref: https://github.com/korthout/backport-action#inputs
|
|
||||||
with:
|
|
||||||
# Use token to allow workflows to be triggered for the created PR
|
|
||||||
github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
|
||||||
# Match labels with a pattern `backport:<target-branch>`
|
|
||||||
label_pattern: '^backport:([^ ]+)$'
|
|
||||||
# A bit shorter pull-request title than the default
|
|
||||||
pull_title: '[${target_branch}] ${pull_title}'
|
|
||||||
# Simpler PR description than default
|
|
||||||
pull_description: |-
|
|
||||||
Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
annotations:
|
||||||
|
- checks:
|
||||||
|
- dangerous-workflow
|
||||||
|
reasons:
|
||||||
|
- reason: not-applicable # This workflow does not run untrusted code, the bot will only backport a code if the a PR was approved and merged into main.
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swapi.ArtifactGenerator
|
||||||
|
|
||||||
|
var artifactGeneratorType = apiType{
|
||||||
|
kind: swapi.ArtifactGeneratorKind,
|
||||||
|
humanKind: "artifactgenerator",
|
||||||
|
groupVersion: swapi.GroupVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
type artifactGeneratorAdapter struct {
|
||||||
|
*swapi.ArtifactGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h artifactGeneratorAdapter) asClientObject() client.Object {
|
||||||
|
return h.ArtifactGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h artifactGeneratorAdapter) deepCopyClientObject() client.Object {
|
||||||
|
return h.ArtifactGenerator.DeepCopy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// swapi.ArtifactGeneratorList
|
||||||
|
|
||||||
|
type artifactGeneratorListAdapter struct {
|
||||||
|
*swapi.ArtifactGeneratorList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h artifactGeneratorListAdapter) asClientList() client.ObjectList {
|
||||||
|
return h.ArtifactGeneratorList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h artifactGeneratorListAdapter) len() int {
|
||||||
|
return len(h.ArtifactGeneratorList.Items)
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
//go:build e2e
|
||||||
|
// +build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateTenant(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no args",
|
||||||
|
args: "create tenant",
|
||||||
|
assert: assertError("name is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no namespace",
|
||||||
|
args: "create tenant dev-team --cluster-role=cluster-admin",
|
||||||
|
assert: assertError("with-namespace is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic tenant",
|
||||||
|
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_tenant/tenant-basic.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tenant with custom serviceaccount",
|
||||||
|
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --with-service-account=flux-tenant --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-service-account.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tenant with custom cluster role",
|
||||||
|
args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exportArtifactCmd = &cobra.Command{
|
||||||
|
Use: "artifact",
|
||||||
|
Short: "Export artifact objects",
|
||||||
|
Long: `The export artifact sub-commands export artifacts objects in YAML format.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
exportCmd.AddCommand(exportArtifactCmd)
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exportArtifactGeneratorCmd = &cobra.Command{
|
||||||
|
Use: "generator [name]",
|
||||||
|
Short: "Export ArtifactGenerator resources in YAML format",
|
||||||
|
Long: "The export artifact generator command exports one or all ArtifactGenerator resources in YAML format.",
|
||||||
|
Example: ` # Export all ArtifactGenerator resources
|
||||||
|
flux export artifact generator --all > artifact-generators.yaml
|
||||||
|
|
||||||
|
# Export a specific generator
|
||||||
|
flux export artifact generator my-generator > my-generator.yaml`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
||||||
|
RunE: exportCommand{
|
||||||
|
object: artifactGeneratorAdapter{&swapi.ArtifactGenerator{}},
|
||||||
|
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
||||||
|
}.run,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
exportArtifactCmd.AddCommand(exportArtifactGeneratorCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export returns an ArtifactGenerator value which has
|
||||||
|
// extraneous information stripped out.
|
||||||
|
func exportArtifactGenerator(item *swapi.ArtifactGenerator) interface{} {
|
||||||
|
gvk := swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)
|
||||||
|
export := swapi.ArtifactGenerator{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: item.Name,
|
||||||
|
Namespace: item.Namespace,
|
||||||
|
Labels: item.Labels,
|
||||||
|
Annotations: item.Annotations,
|
||||||
|
},
|
||||||
|
Spec: item.Spec,
|
||||||
|
}
|
||||||
|
return export
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex artifactGeneratorAdapter) export() interface{} {
|
||||||
|
return exportArtifactGenerator(ex.ArtifactGenerator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex artifactGeneratorListAdapter) exportItem(i int) interface{} {
|
||||||
|
return exportArtifactGenerator(&ex.ArtifactGeneratorList.Items[i])
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getArtifactCmd = &cobra.Command{
|
||||||
|
Use: "artifacts",
|
||||||
|
Aliases: []string{"artifact"},
|
||||||
|
Short: "Get artifact object status",
|
||||||
|
Long: `The get artifact sub-commands print the status of artifact objects.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getCmd.AddCommand(getArtifactCmd)
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getArtifactGeneratorCmd = &cobra.Command{
|
||||||
|
Use: "generators",
|
||||||
|
Aliases: []string{"generator"},
|
||||||
|
Short: "Get artifact generator statuses",
|
||||||
|
Long: `The get artifact generator command prints the statuses of the resources.`,
|
||||||
|
Example: ` # List all ArtifactGenerators and their status
|
||||||
|
flux get artifact generators`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
get := getCommand{
|
||||||
|
apiType: receiverType,
|
||||||
|
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
||||||
|
funcMap: make(typeMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
|
||||||
|
o, ok := obj.(*swapi.ArtifactGenerator)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("impossible to cast type %#v generator", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink := artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{
|
||||||
|
Items: []swapi.ArtifactGenerator{
|
||||||
|
*o,
|
||||||
|
}}}
|
||||||
|
return sink, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := get.run(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getArtifactCmd.AddCommand(getArtifactGeneratorCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s artifactGeneratorListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
|
item := s.Items[i]
|
||||||
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
|
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||||
|
cases.Title(language.English).String(strconv.FormatBool(item.IsDisabled())), status, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s artifactGeneratorListAdapter) headers(includeNamespace bool) []string {
|
||||||
|
headers := []string{"Name", "Suspended", "Ready", "Message"}
|
||||||
|
if includeNamespace {
|
||||||
|
return append(namespaceHeader, headers...)
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s artifactGeneratorListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||||
|
item := s.Items[i]
|
||||||
|
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getSourceExternalCmd = &cobra.Command{
|
||||||
|
Use: "external",
|
||||||
|
Short: "Get ExternalArtifact source statuses",
|
||||||
|
Long: `The get sources external command prints the status of the ExternalArtifact sources.`,
|
||||||
|
Example: ` # List all ExternalArtifacts and their status
|
||||||
|
flux get sources external
|
||||||
|
|
||||||
|
# List ExternalArtifacts from all namespaces
|
||||||
|
flux get sources external --all-namespaces`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
get := getCommand{
|
||||||
|
apiType: externalArtifactType,
|
||||||
|
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||||
|
funcMap: make(typeMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
|
||||||
|
o, ok := obj.(*sourcev1.ExternalArtifact)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("impossible to cast type %#v to ExternalArtifact", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink := &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{
|
||||||
|
Items: []sourcev1.ExternalArtifact{
|
||||||
|
*o,
|
||||||
|
}}}
|
||||||
|
return sink, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := get.run(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getSourceCmd.AddCommand(getSourceExternalCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *externalArtifactListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
|
item := a.Items[i]
|
||||||
|
var revision string
|
||||||
|
if item.Status.Artifact != nil {
|
||||||
|
revision = item.Status.Artifact.Revision
|
||||||
|
}
|
||||||
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
|
revision = utils.TruncateHex(revision)
|
||||||
|
msg = utils.TruncateHex(msg)
|
||||||
|
|
||||||
|
var source string
|
||||||
|
if item.Spec.SourceRef != nil {
|
||||||
|
source = fmt.Sprintf("%s/%s/%s",
|
||||||
|
item.Spec.SourceRef.Kind,
|
||||||
|
item.Spec.SourceRef.Namespace,
|
||||||
|
item.Spec.SourceRef.Name)
|
||||||
|
}
|
||||||
|
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||||
|
revision, source, status, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) headers(includeNamespace bool) []string {
|
||||||
|
headers := []string{"Name", "Revision", "Source", "Ready", "Message"}
|
||||||
|
if includeNamespace {
|
||||||
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||||
|
item := a.Items[i]
|
||||||
|
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
|
||||||
|
}
|
@ -0,0 +1,691 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/ssa"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
|
imageautov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||||
|
imageautov1b2 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||||
|
imagev1b2 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
|
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
|
swv1b1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIVersions holds the mapping of GroupKinds to their respective
|
||||||
|
// latest API versions for a specific Flux version.
|
||||||
|
type APIVersions struct {
|
||||||
|
FluxVersion string
|
||||||
|
LatestVersions map[schema.GroupKind]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update this mapping when new Flux minor versions are released!
|
||||||
|
// latestAPIVersions contains the latest API versions for each GroupKind
|
||||||
|
// for each supported Flux version. We maintain the latest two minor versions.
|
||||||
|
var latestAPIVersions = []APIVersions{
|
||||||
|
{
|
||||||
|
FluxVersion: "2.7",
|
||||||
|
LatestVersions: map[schema.GroupKind]string{
|
||||||
|
// source-controller
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// kustomize-controller
|
||||||
|
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// helm-controller
|
||||||
|
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// notification-controller
|
||||||
|
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-reflector-controller
|
||||||
|
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version,
|
||||||
|
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-automation-controller
|
||||||
|
{Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// source-watcher
|
||||||
|
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
FluxVersion: "2.6",
|
||||||
|
LatestVersions: map[schema.GroupKind]string{
|
||||||
|
// source-controller
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// kustomize-controller
|
||||||
|
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// helm-controller
|
||||||
|
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// notification-controller
|
||||||
|
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-reflector-controller
|
||||||
|
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version,
|
||||||
|
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-automation-controller
|
||||||
|
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateCmd = &cobra.Command{
|
||||||
|
Use: "migrate",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Short: "Migrate the Flux custom resources to their latest API version",
|
||||||
|
Long: `The migrate command must be run before a Flux minor version upgrade.
|
||||||
|
|
||||||
|
The command has two modes of operation:
|
||||||
|
|
||||||
|
- Cluster mode (default): migrates all the Flux custom resources stored in Kubernetes etcd to their latest API version.
|
||||||
|
- File system mode (-f): migrates the Flux custom resources defined in the manifests located in the specified path.
|
||||||
|
`,
|
||||||
|
Example: ` # Migrate all the Flux custom resources in the cluster.
|
||||||
|
# This uses the current kubeconfig context and requires cluster-admin permissions.
|
||||||
|
flux migrate
|
||||||
|
|
||||||
|
# Migrate all the Flux custom resources in a Git repository
|
||||||
|
# checked out in the current working directory.
|
||||||
|
flux migrate -f .
|
||||||
|
|
||||||
|
# Migrate all Flux custom resources defined in YAML and Helm YAML template files.
|
||||||
|
flux migrate -f . --extensions=.yml,.yaml,.tpl
|
||||||
|
|
||||||
|
# Migrate the Flux custom resources to the latest API versions of Flux 2.6.
|
||||||
|
flux migrate -f . --version=2.6
|
||||||
|
|
||||||
|
# Migrate the Flux custom resources defined in a multi-document YAML manifest file.
|
||||||
|
flux migrate -f path/to/manifest.yaml
|
||||||
|
|
||||||
|
# Simulate the migration without making any changes.
|
||||||
|
flux migrate -f . --dry-run
|
||||||
|
|
||||||
|
# Run the migration skipping confirmation prompts.
|
||||||
|
flux migrate -f . --yes
|
||||||
|
`,
|
||||||
|
RunE: runMigrateCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateFlags struct {
|
||||||
|
yes bool
|
||||||
|
dryRun bool
|
||||||
|
path string
|
||||||
|
version string
|
||||||
|
extensions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(migrateCmd)
|
||||||
|
|
||||||
|
migrateCmd.Flags().StringVarP(&migrateFlags.path, "path", "f", "",
|
||||||
|
"the path to the directory containing the manifests to migrate")
|
||||||
|
migrateCmd.Flags().StringSliceVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"},
|
||||||
|
"the file extensions to consider when migrating manifests, only applicable with --path")
|
||||||
|
migrateCmd.Flags().StringVarP(&migrateFlags.version, "version", "v", "",
|
||||||
|
"the target Flux minor version to migrate manifests to, only applicable with --path (defaults to the version of the CLI)")
|
||||||
|
migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false,
|
||||||
|
"skip confirmation prompts when migrating manifests, only applicable with --path")
|
||||||
|
migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false,
|
||||||
|
"simulate the migration of manifests without making any changes, only applicable with --path")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateCmd(*cobra.Command, []string) error {
|
||||||
|
if migrateFlags.path == "" {
|
||||||
|
return migrateCluster()
|
||||||
|
}
|
||||||
|
return migrateFileSystem()
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCluster() error {
|
||||||
|
logger.Actionf("starting migration of custom resources")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("the Kubernetes client initialization failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := NewClusterMigrator(kubeClient, client.MatchingLabels{
|
||||||
|
"app.kubernetes.io/part-of": "flux",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := migrator.Run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("custom resources migrated successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateFileSystem() error {
|
||||||
|
pathRoot, err := os.OpenRoot(".")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open filesystem at the current working directory: %w", err)
|
||||||
|
}
|
||||||
|
defer pathRoot.Close()
|
||||||
|
|
||||||
|
fileSystem := &osFS{pathRoot.FS()}
|
||||||
|
yes := migrateFlags.yes
|
||||||
|
dryRun := migrateFlags.dryRun
|
||||||
|
path := migrateFlags.path
|
||||||
|
extensions := migrateFlags.extensions
|
||||||
|
var latestVersions map[schema.GroupKind]string
|
||||||
|
|
||||||
|
// Determine latest API versions based on the Flux version.
|
||||||
|
if migrateFlags.version == "" {
|
||||||
|
latestVersions = latestAPIVersions[0].LatestVersions
|
||||||
|
} else {
|
||||||
|
supportedVersions := make([]string, 0, len(latestAPIVersions))
|
||||||
|
for _, v := range latestAPIVersions {
|
||||||
|
if v.FluxVersion == migrateFlags.version {
|
||||||
|
latestVersions = v.LatestVersions
|
||||||
|
break
|
||||||
|
}
|
||||||
|
supportedVersions = append(supportedVersions, v.FluxVersion)
|
||||||
|
}
|
||||||
|
if latestVersions == nil {
|
||||||
|
return fmt.Errorf("version %s is not supported, supported versions are: %s",
|
||||||
|
migrateFlags.version, strings.Join(supportedVersions, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFileSystemMigrator(fileSystem, yes, dryRun, path, extensions, latestVersions).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterMigrator migrates all the CRs in the cluster for the CRDs matching the label selector.
|
||||||
|
type ClusterMigrator struct {
|
||||||
|
labelSelector client.MatchingLabels
|
||||||
|
kubeClient client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterMigrator creates a new ClusterMigrator instance with the specified label selector.
|
||||||
|
func NewClusterMigrator(kubeClient client.Client, labelSelector client.MatchingLabels) *ClusterMigrator {
|
||||||
|
return &ClusterMigrator{
|
||||||
|
labelSelector: labelSelector,
|
||||||
|
kubeClient: kubeClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterMigrator) Run(ctx context.Context) error {
|
||||||
|
crdList := &apiextensionsv1.CustomResourceDefinitionList{}
|
||||||
|
|
||||||
|
if err := c.kubeClient.List(ctx, crdList, c.labelSelector); err != nil {
|
||||||
|
return fmt.Errorf("failed to list CRDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, crd := range crdList.Items {
|
||||||
|
if err := c.migrateCRD(ctx, crd.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterMigrator) migrateCRD(ctx context.Context, name string) error {
|
||||||
|
crd := &apiextensionsv1.CustomResourceDefinition{}
|
||||||
|
|
||||||
|
if err := c.kubeClient.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil {
|
||||||
|
return fmt.Errorf("failed to get CRD %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the latest storage version for the CRD
|
||||||
|
storageVersion := c.getStorageVersion(crd)
|
||||||
|
if storageVersion == "" {
|
||||||
|
return fmt.Errorf("no storage version found for CRD %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate all the resources for the CRD
|
||||||
|
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||||
|
return c.migrateCR(ctx, crd, storageVersion)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate resources for CRD %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the CRD status to contain only the latest storage version
|
||||||
|
if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != storageVersion {
|
||||||
|
crd.Status.StoredVersions = []string{storageVersion}
|
||||||
|
if err := c.kubeClient.Status().Update(ctx, crd); err != nil {
|
||||||
|
return fmt.Errorf("failed to update CRD %s status: %w", crd.Name, err)
|
||||||
|
}
|
||||||
|
logger.Successf("%s migrated to storage version %s", crd.Name, storageVersion)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateCR migrates all CRs for the given CRD to the specified version by patching them.
|
||||||
|
func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error {
|
||||||
|
list := &unstructured.UnstructuredList{}
|
||||||
|
|
||||||
|
apiVersion := crd.Spec.Group + "/" + version
|
||||||
|
listKind := crd.Spec.Names.ListKind
|
||||||
|
|
||||||
|
list.SetAPIVersion(apiVersion)
|
||||||
|
list.SetKind(listKind)
|
||||||
|
|
||||||
|
err := c.kubeClient.List(ctx, list, client.InNamespace(""))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list resources for CRD %s: %w", crd.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(list.Items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range list.Items {
|
||||||
|
patches, err := ssa.PatchMigrateToVersion(&item, apiVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration patch for %s/%s/%s: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(patches) == 0 {
|
||||||
|
// patch the resource with an empty patch to update the version
|
||||||
|
if err := c.kubeClient.Patch(
|
||||||
|
ctx,
|
||||||
|
&item,
|
||||||
|
client.RawPatch(client.Merge.Type(), []byte("{}")),
|
||||||
|
); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
return fmt.Errorf(" %s/%s/%s failed to migrate: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// patch the resource to migrate the managed fields to the latest apiVersion
|
||||||
|
rawPatch, err := json.Marshal(patches)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal migration patch for %s/%s/%s: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
if err := c.kubeClient.Patch(
|
||||||
|
ctx,
|
||||||
|
&item,
|
||||||
|
client.RawPatch(types.JSONPatchType, rawPatch),
|
||||||
|
); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
return fmt.Errorf(" %s/%s/%s failed to migrate managed fields: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("%s/%s/%s migrated to version %s",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStorageVersion retrieves the storage version of a CustomResourceDefinition.
|
||||||
|
func (c *ClusterMigrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefinition) string {
|
||||||
|
var version string
|
||||||
|
for _, v := range crd.Spec.Versions {
|
||||||
|
if v.Storage {
|
||||||
|
version = v.Name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritableFS extends fs.FS with a WriteFile method.
|
||||||
|
type WritableFS interface {
|
||||||
|
fs.FS
|
||||||
|
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// osFS is a WritableFS implementation that uses the file system of the OS.
|
||||||
|
type osFS struct {
|
||||||
|
fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *osFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
return os.WriteFile(name, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSystemMigrator migrates all the CRs found in the manifests located in the specified path.
|
||||||
|
type FileSystemMigrator struct {
|
||||||
|
fileSystem WritableFS
|
||||||
|
yes bool
|
||||||
|
dryRun bool
|
||||||
|
path string
|
||||||
|
extensions []string
|
||||||
|
latestVersions map[schema.GroupKind]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileAPIUpgrades represents the API upgrades detected in a specific manifest file.
|
||||||
|
type FileAPIUpgrades struct {
|
||||||
|
File string
|
||||||
|
Upgrades []APIUpgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIUpgrade represents an upgrade of a specific API version in a manifest file.
|
||||||
|
type APIUpgrade struct {
|
||||||
|
Line int
|
||||||
|
Kind string
|
||||||
|
OldVersion string
|
||||||
|
NewVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileSystemMigrator creates a new FileSystemMigrator instance with the specified flags.
|
||||||
|
func NewFileSystemMigrator(fileSystem WritableFS, yes, dryRun bool, path string,
|
||||||
|
extensions []string, latestVersions map[schema.GroupKind]string) *FileSystemMigrator {
|
||||||
|
return &FileSystemMigrator{
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
yes: yes,
|
||||||
|
dryRun: dryRun,
|
||||||
|
path: filepath.Clean(path), // convert dir/ to dir to avoid error when walking
|
||||||
|
extensions: extensions,
|
||||||
|
latestVersions: latestVersions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) Run() error {
|
||||||
|
logger.Actionf("starting migration of custom resources")
|
||||||
|
|
||||||
|
// List and filter files.
|
||||||
|
files, err := f.listFiles()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect upgrades.
|
||||||
|
upgrades, err := f.detectUpgrades(files)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(upgrades) == 0 {
|
||||||
|
logger.Successf("no custom resources found that require migration")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if f.dryRun {
|
||||||
|
logger.Successf("dry-run mode enabled, no changes will be made")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm upgrades.
|
||||||
|
if !f.yes {
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: "Are you sure you want to proceed with the above upgrades", // Already prints "? [y/N]"
|
||||||
|
IsConfirm: true,
|
||||||
|
}
|
||||||
|
if _, err := prompt.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate files.
|
||||||
|
for _, fileUpgrades := range upgrades {
|
||||||
|
if err := f.migrateFile(&fileUpgrades); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Successf("file %s migrated successfully", fileUpgrades.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("custom resources migrated successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) listFiles() ([]string, error) {
|
||||||
|
fileInfo, err := fs.Stat(f.fileSystem, f.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to stat path %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
return f.listDirectoryFiles()
|
||||||
|
}
|
||||||
|
if err := f.validateSingleFile(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []string{f.path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) listDirectoryFiles() ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
err := fs.WalkDir(f.fileSystem, f.path, func(path string, dirEntry fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !f.matchesExtensions(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fileInfo, err := dirEntry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fileInfo.Mode().IsRegular() {
|
||||||
|
files = append(files, path)
|
||||||
|
} else if !fileInfo.IsDir() {
|
||||||
|
logger.Warningf("skipping irregular file %s", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) validateSingleFile() error {
|
||||||
|
if !f.matchesExtensions(f.path) {
|
||||||
|
return fmt.Errorf("file %s does not match the specified extensions: %v",
|
||||||
|
f.path, strings.Join(f.extensions, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's irregular by walking the parent directory.
|
||||||
|
var irregular bool
|
||||||
|
err := fs.WalkDir(f.fileSystem, filepath.Dir(f.path), func(path string, dirEntry fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path != f.path {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fileInfo, err := dirEntry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !fileInfo.Mode().IsRegular() {
|
||||||
|
irregular = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to validate file %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
if irregular {
|
||||||
|
return fmt.Errorf("file %s is irregular", f.path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) matchesExtensions(file string) bool {
|
||||||
|
for _, ext := range f.extensions {
|
||||||
|
if strings.HasSuffix(file, ext) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) detectUpgrades(files []string) ([]FileAPIUpgrades, error) {
|
||||||
|
var upgrades []FileAPIUpgrades
|
||||||
|
for _, file := range files {
|
||||||
|
fileUpgrades, err := f.detectFileUpgrades(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(fileUpgrades) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fu := FileAPIUpgrades{
|
||||||
|
File: file,
|
||||||
|
Upgrades: fileUpgrades,
|
||||||
|
}
|
||||||
|
upgrades = append(upgrades, fu)
|
||||||
|
f.printDetectedUpgrades(&fu)
|
||||||
|
}
|
||||||
|
return upgrades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) detectFileUpgrades(file string) ([]APIUpgrade, error) {
|
||||||
|
b, err := fs.ReadFile(f.fileSystem, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read file %s: %w", file, err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
|
||||||
|
var fileUpgrades []APIUpgrade
|
||||||
|
for line, apiVersionLine := range lines {
|
||||||
|
// Parse apiVersion.
|
||||||
|
const apiVersionPrefix = "apiVersion: "
|
||||||
|
idx := strings.Index(apiVersionLine, apiVersionPrefix)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiVersionValuePrefix := strings.TrimSpace(apiVersionLine[idx+len(apiVersionPrefix):])
|
||||||
|
apiVersion := strings.Split(apiVersionValuePrefix, " ")[0]
|
||||||
|
gv, err := schema.ParseGroupVersion(apiVersion)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("%s:%d: %v", file, line+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse kind.
|
||||||
|
if line+1 >= len(lines) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kindLine := lines[line+1]
|
||||||
|
const kindPrefix = "kind: "
|
||||||
|
idx = strings.Index(kindLine, kindPrefix)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kindValuePrefix := strings.TrimSpace(kindLine[idx+len(kindPrefix):])
|
||||||
|
kind := strings.Split(kindValuePrefix, " ")[0]
|
||||||
|
|
||||||
|
// Build GroupKind.
|
||||||
|
gk := schema.GroupKind{
|
||||||
|
Group: gv.Group,
|
||||||
|
Kind: kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a newer version for the GroupKind.
|
||||||
|
latestVersion, ok := f.latestVersions[gk]
|
||||||
|
if !ok || latestVersion == gv.Version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the upgrade.
|
||||||
|
fileUpgrades = append(fileUpgrades, APIUpgrade{
|
||||||
|
Line: line,
|
||||||
|
Kind: kind,
|
||||||
|
OldVersion: gv.Version,
|
||||||
|
NewVersion: latestVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fileUpgrades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) printDetectedUpgrades(fileUpgrades *FileAPIUpgrades) {
|
||||||
|
for _, upgrade := range fileUpgrades.Upgrades {
|
||||||
|
logger.Generatef("%s:%d: %s %s -> %s",
|
||||||
|
fileUpgrades.File,
|
||||||
|
upgrade.Line+1,
|
||||||
|
upgrade.Kind,
|
||||||
|
upgrade.OldVersion,
|
||||||
|
upgrade.NewVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) migrateFile(fileUpgrades *FileAPIUpgrades) error {
|
||||||
|
// Read file and map lines.
|
||||||
|
b, err := fs.ReadFile(f.fileSystem, fileUpgrades.File)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
|
||||||
|
// Apply upgrades to lines.
|
||||||
|
for _, upgrade := range fileUpgrades.Upgrades {
|
||||||
|
line := lines[upgrade.Line]
|
||||||
|
line = strings.Replace(line, upgrade.OldVersion, upgrade.NewVersion, 1)
|
||||||
|
lines[upgrade.Line] = line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file info to preserve permissions.
|
||||||
|
fileInfo, err := fs.Stat(f.fileSystem, fileUpgrades.File)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file with preserved permissions.
|
||||||
|
b = []byte(strings.Join(lines, "\n"))
|
||||||
|
if err := f.fileSystem.WriteFile(fileUpgrades.File, b, fileInfo.Mode()); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writeToMemoryFS struct {
|
||||||
|
fs.FS
|
||||||
|
|
||||||
|
writtenFiles map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *writeToMemoryFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
m.writtenFiles[name] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type writtenFile struct {
|
||||||
|
file string
|
||||||
|
goldenFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSystemMigrator(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
outputGolden string
|
||||||
|
writtenFiles []writtenFile
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "errors out for single file that is a symlink",
|
||||||
|
path: "testdata/migrate/file-system/single-file-link.yaml",
|
||||||
|
err: "file testdata/migrate/file-system/single-file-link.yaml is irregular",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "errors out for single file with wrong extension",
|
||||||
|
path: "testdata/migrate/file-system/single-file-wrong-ext.json",
|
||||||
|
err: "file testdata/migrate/file-system/single-file-wrong-ext.json does not match the specified extensions: .yaml, .yml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrate single file",
|
||||||
|
path: "testdata/migrate/file-system/single-file.yaml",
|
||||||
|
outputGolden: "testdata/migrate/file-system/single-file.yaml.output.golden",
|
||||||
|
writtenFiles: []writtenFile{
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/single-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/single-file.yaml.golden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrate files in directory",
|
||||||
|
path: "testdata/migrate/file-system/dir",
|
||||||
|
outputGolden: "testdata/migrate/file-system/dir.output.golden",
|
||||||
|
writtenFiles: []writtenFile{
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-dir/another-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-dir/another-file.yml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-file.yml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Store logger, replace with test logger, and restore at the end of the test.
|
||||||
|
var testLogger bytes.Buffer
|
||||||
|
oldLogger := logger
|
||||||
|
logger = stderrLogger{&testLogger}
|
||||||
|
t.Cleanup(func() { logger = oldLogger })
|
||||||
|
|
||||||
|
// Open current working directory as root and build write-to-memory filesystem.
|
||||||
|
pathRoot, err := os.OpenRoot(".")
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
t.Cleanup(func() { pathRoot.Close() })
|
||||||
|
fileSystem := &writeToMemoryFS{
|
||||||
|
FS: pathRoot.FS(),
|
||||||
|
writtenFiles: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare other inputs.
|
||||||
|
const yes = true
|
||||||
|
const dryRun = false
|
||||||
|
extensions := []string{".yaml", ".yml"}
|
||||||
|
latestVersions := map[schema.GroupKind]string{
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImageRepository"}: "v1",
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImagePolicy"}: "v1",
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImageUpdateAutomation"}: "v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration.
|
||||||
|
err = NewFileSystemMigrator(fileSystem, yes, dryRun, tt.path, extensions, latestVersions).Run()
|
||||||
|
if tt.err != "" {
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
g.Expect(err.Error()).To(Equal(tt.err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Assert logger output.
|
||||||
|
b, err := os.ReadFile(tt.outputGolden)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(string(b)).To(Equal(testLogger.String()),
|
||||||
|
"logger output does not match golden file %s", tt.outputGolden)
|
||||||
|
|
||||||
|
// Assert which files were written.
|
||||||
|
writtenFiles := make([]string, 0, len(fileSystem.writtenFiles))
|
||||||
|
for name := range fileSystem.writtenFiles {
|
||||||
|
writtenFiles = append(writtenFiles, name)
|
||||||
|
}
|
||||||
|
expectedWrittenFiles := make([]string, 0, len(tt.writtenFiles))
|
||||||
|
for _, wf := range tt.writtenFiles {
|
||||||
|
expectedWrittenFiles = append(expectedWrittenFiles, wf.file)
|
||||||
|
}
|
||||||
|
g.Expect(writtenFiles).To(ConsistOf(expectedWrittenFiles))
|
||||||
|
|
||||||
|
// Assert contents of written files.
|
||||||
|
for _, wf := range tt.writtenFiles {
|
||||||
|
b, err := os.ReadFile(wf.goldenFile)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(string(fileSystem.writtenFiles[wf.file])).To(Equal(string(b)),
|
||||||
|
"file %s does not match golden file %s", wf.file, wf.goldenFile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
"github.com/google/go-containerregistry/pkg/crane"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/auth"
|
||||||
|
"github.com/fluxcd/pkg/auth/azure"
|
||||||
|
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
||||||
|
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, authn.Authenticator, error) {
|
||||||
|
var opts []auth.Option
|
||||||
|
if provider == azure.ProviderName {
|
||||||
|
opts = append(opts, auth.WithAllowShellOut())
|
||||||
|
}
|
||||||
|
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err)
|
||||||
|
}
|
||||||
|
return crane.WithAuth(authenticator), authenticator, nil
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reconcileImagePolicyCmd = &cobra.Command{
|
||||||
|
Use: "policy [name]",
|
||||||
|
Short: "Reconcile an ImagePolicy",
|
||||||
|
Long: `The reconcile image policy command triggers a reconciliation of an ImagePolicy resource and waits for it to finish.`,
|
||||||
|
Example: `
|
||||||
|
# Trigger a reconciliation for an existing image policy called 'alpine'
|
||||||
|
flux reconcile image policy alpine`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)),
|
||||||
|
RunE: reconcileCommand{
|
||||||
|
apiType: imagePolicyType,
|
||||||
|
object: imagePolicyAdapter{&imagev1.ImagePolicy{}},
|
||||||
|
}.run,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
reconcileImageCmd.AddCommand(reconcileImagePolicyCmd)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue