Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kubelogin for openSUSE:Factory checked in at 2026-10-27 17:07:34 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kubelogin (Old) and /work/SRC/openSUSE:Factory/.kubelogin.new.2252680 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kubelogin" Tue Oct 27 17:07:34 2026 rev:32 rq:1393048 version:0.2.22 Changes: -------- --- /work/SRC/openSUSE:Factory/kubelogin/kubelogin.changes 2026-10-09 21:45:00.049352108 +0200 +++ /work/SRC/openSUSE:Factory/.kubelogin.new.2252680/kubelogin.changes 2026-10-27 17:08:18.230081857 +0100 @@ -1,0 +2,9 @@ +Tue Oct 27 06:57:55 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.2.22: + * What's Changed + - Add --outfile flag to convert-kubeconfig in #823 + * Maintenance + - Bump Go to 1.26.10 for CVE-2026-111943 in #824 + +------------------------------------------------------------------- Old: ---- kubelogin-0.2.21.obscpio New: ---- kubelogin-0.2.22.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kubelogin.spec ++++++ --- /var/tmp/diff_new_pack.ZkyBeN/_old 2026-10-27 17:08:20.394172505 +0100 +++ /var/tmp/diff_new_pack.ZkyBeN/_new 2026-10-27 17:08:20.396172588 +0100 @@ -17,14 +17,14 @@ Name: kubelogin -Version: 0.2.21 +Version: 0.2.22 Release: 0 Summary: Kubernetes client credential plugin implementing Azure authentication License: MIT URL: https://github.com/Azure/kubelogin Source0: %{name}-%{version}.tar.gz Source1: vendor.tar.gz -BuildRequires: go1.26 >= 1.26.6 +BuildRequires: go1.26 >= 1.26.10 BuildRequires: golang-packaging %{go_provides} ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ZkyBeN/_old 2026-10-27 17:08:20.449174808 +0100 +++ /var/tmp/diff_new_pack.ZkyBeN/_new 2026-10-27 17:08:20.453174976 +0100 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/Azure/kubelogin.git</param> <param name="scm">git</param> - <param name="revision">refs/tags/v0.2.21</param> + <param name="revision">refs/tags/v0.2.22</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.ZkyBeN/_old 2026-10-27 17:08:20.486176358 +0100 +++ /var/tmp/diff_new_pack.ZkyBeN/_new 2026-10-27 17:08:20.490176526 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/Azure/kubelogin.git</param> - <param name="changesrevision">532826311d3a5458df998a2cf910f5dec2a66e17</param></service></servicedata> + <param name="changesrevision">f341e3167eb2c077fd95f73b3c454236262299ce</param></service></servicedata> (No newline at EOF) ++++++ kubelogin-0.2.21.obscpio -> kubelogin-0.2.22.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/CHANGELOG.md new/kubelogin-0.2.22/CHANGELOG.md --- old/kubelogin-0.2.21/CHANGELOG.md 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/CHANGELOG.md 2026-10-24 06:18:44.000000000 +0200 @@ -1,5 +1,21 @@ # Change Log +## [0.2.22] + +### What's Changed + +* Add `--outfile` flag to convert-kubeconfig by @akshat-lakhera in https://github.com/Azure/kubelogin/pull/823 + +### Maintenance + +* Bump Go to 1.26.10 for CVE-2026-111943 by @weinong in https://github.com/Azure/kubelogin/pull/824 + +### New Contributors + +* @akshat-lakhera made their first contribution in https://github.com/Azure/kubelogin/pull/823 + +**Full Changelog**: https://github.com/Azure/kubelogin/compare/v0.2.21...v0.2.22 + ## [0.2.21] ### Maintenance diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/docs/book/src/cli/convert-kubeconfig.md new/kubelogin-0.2.22/docs/book/src/cli/convert-kubeconfig.md --- old/kubelogin-0.2.21/docs/book/src/cli/convert-kubeconfig.md 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/docs/book/src/cli/convert-kubeconfig.md 2026-10-24 06:18:44.000000000 +0200 @@ -4,6 +4,12 @@ Note that when `--context` is specified, only the matching kubeconfig context will be converted. Otherwise, every kubeconfig context that uses azure auth or Exec plugin will be converted. +By default, the kubeconfig is converted in place. Use `--outfile` to write the converted kubeconfig to a new file and leave the source kubeconfig untouched. When `KUBECONFIG` lists several files, the outfile gets the merged and converted result. + +```sh +kubelogin convert-kubeconfig -l azurecli --kubeconfig ~/.kube/config --outfile ./azurecli-kubeconfig +``` + ## Usage ```sh @@ -33,6 +39,7 @@ --legacy set to true to get token with 'spn:' prefix in audience claim -l, --login string Login method. Supported methods: devicecode, interactive, spn, ropc, msi, azurecli, azd, workloadidentity, azurepipelines. It may be specified in AAD_LOGIN_METHOD environment variable (default "devicecode") --login-hint string The login hint to pre-fill the username in the interactive login flow. + --outfile string Write the converted kubeconfig to this file instead of modifying the source kubeconfig --password string password for ropc login flow. It may be specified in AAD_USER_PRINCIPAL_PASSWORD or AZURE_PASSWORD environment variable --pop-claims key=val,key2=val2 contains a comma-separated list of claims to attach to the pop token in the format key=val,key2=val2. At minimum, specify the ARM ID of the cluster as `u=ARM_ID` --pop-enabled set to true to use a PoP token for authentication or false to use a regular bearer token diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/go.mod new/kubelogin-0.2.22/go.mod --- old/kubelogin-0.2.21/go.mod 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/go.mod 2026-10-24 06:18:44.000000000 +0200 @@ -1,7 +1,7 @@ module github.com/Azure/kubelogin // NOTE: kubelogin follows the same support policy as Go, which supports the last two major versions. -go 1.26.6 +go 1.26.10 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/pkg/internal/converter/convert.go new/kubelogin-0.2.22/pkg/internal/converter/convert.go --- old/kubelogin-0.2.21/pkg/internal/converter/convert.go 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/pkg/internal/converter/convert.go 2026-10-24 06:18:44.000000000 +0200 @@ -2,6 +2,7 @@ import ( "fmt" + "os" "strings" "k8s.io/client-go/tools/clientcmd" @@ -46,6 +47,7 @@ flagAzureConfigDir = "azure-config-dir" flagClientID = "client-id" flagContext = "context" + flagOutFile = "outfile" flagServerID = "server-id" flagTenantID = "tenant-id" flagSubscriptionID = "subscription" @@ -494,10 +496,46 @@ authInfo.Exec = exec authInfo.AuthProvider = nil } + + if o.outFile != "" { + // write the whole (possibly merged) converted kubeconfig to a new file and leave the source files untouched + if len(config.Contexts) == 0 && len(config.AuthInfos) == 0 { + return fmt.Errorf("no kubeconfig found to convert, refusing to write an empty %s", o.outFile) + } + if err := rejectSourceOutFile(o.outFile, kubeconfigs); err != nil { + return err + } + klog.V(5).Infof("Writing converted kubeconfig to %s", o.outFile) + if err := clientcmd.WriteToFile(config, o.outFile); err != nil { + return fmt.Errorf("unable to write kubeconfig to %s: %w", o.outFile, err) + } + return nil + } + err = clientcmd.ModifyConfig(pathOptions, config, true) return err } +// rejectSourceOutFile returns an error if outFile is the same file as any of the source kubeconfigs, +// including through symlinks and hard links, so that the sources are never overwritten. +func rejectSourceOutFile(outFile string, sources []string) error { + outInfo, err := os.Stat(outFile) + if err != nil { + // a file that does not exist yet can not alias a source + return nil + } + for _, source := range sources { + sourceInfo, err := os.Stat(source) + if err != nil { + continue + } + if os.SameFile(outInfo, sourceInfo) { + return fmt.Errorf("--%s %q is the same file as source kubeconfig %q; choose a different path", flagOutFile, outFile, source) + } + } + return nil +} + // get the item in Exec.Args[] right after someArg func getExecArg(authInfoPtr *api.AuthInfo, someArg string) (resultStr string) { if someArg == "" { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/pkg/internal/converter/convert_test.go new/kubelogin-0.2.22/pkg/internal/converter/convert_test.go --- old/kubelogin-0.2.21/pkg/internal/converter/convert_test.go 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/pkg/internal/converter/convert_test.go 2026-10-24 06:18:44.000000000 +0200 @@ -1,8 +1,12 @@ package converter import ( + "bytes" + "fmt" "os" "path/filepath" + "runtime" + "strings" "testing" "github.com/Azure/kubelogin/pkg/internal/token" @@ -1768,3 +1772,271 @@ } return false } + +func TestConvertOutFile(t *testing.T) { + const ( + serverID = "serverID" + clientID = "clientID" + tenantID = "tenantID" + caFile = "ca.crt" + ) + authProviderConfig := map[string]string{ + cfgApiserverID: serverID, + cfgClientID: clientID, + cfgTenantID: tenantID, + cfgConfigMode: "1", + } + expectedArgs := []string{ + getTokenCommand, + argServerID, serverID, + argClientID, clientID, + argTenantID, tenantID, + argLoginMethod, token.DeviceCodeLogin, + } + + testData := []struct { + name string + // sources holds the user names stored in each source kubeconfig file. + // More than one entry loads them as a merged KUBECONFIG. + sources [][2]string + // explicitKubeconfig passes the first source with --kubeconfig instead of KUBECONFIG + explicitKubeconfig bool + // missingKubeconfig points KUBECONFIG at a file that does not exist + missingKubeconfig bool + // outFile is relative to the test directory; empty leaves --outfile unset + outFile string + // alias makes outFile a "symlink" or "hardlink" to the first source before converting + alias string + context string + expectedError string + }{ + { + name: "outfile with --kubeconfig leaves the source untouched", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: "converted.yaml", + }, + { + name: "outfile with merged KUBECONFIG files gets the merged result", + sources: [][2]string{{"aks1", "aks2"}, {"aks3", "aks4"}}, + outFile: "converted.yaml", + }, + { + name: "outfile with --context converts only the targeted user", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: "converted.yaml", + context: "aks1", + }, + { + name: "outfile in a missing directory creates the directory", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: filepath.Join("generated", "team", "config"), + }, + { + name: "outfile whose parent is a regular file fails", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: filepath.Join("source-0", "config", "converted.yaml"), + expectedError: "unable to write kubeconfig to", + }, + { + name: "outfile that is the --kubeconfig source fails", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: filepath.Join("source-0", "config"), + expectedError: "is the same file as source kubeconfig", + }, + { + name: "outfile that is a symlink to the source fails", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: "link.yaml", + alias: "symlink", + expectedError: "is the same file as source kubeconfig", + }, + { + name: "outfile that is a hard link to the source fails", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: "link.yaml", + alias: "hardlink", + expectedError: "is the same file as source kubeconfig", + }, + { + name: "outfile that is one of the merged KUBECONFIG files fails", + sources: [][2]string{{"aks1", "aks2"}, {"aks3", "aks4"}}, + outFile: filepath.Join("source-1", "config"), + expectedError: "is the same file as source kubeconfig", + }, + { + name: "outfile that is an existing directory fails", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + outFile: "source-0", + expectedError: "unable to write kubeconfig to", + }, + { + name: "outfile with no kubeconfig to convert fails and writes nothing", + missingKubeconfig: true, + outFile: "converted.yaml", + expectedError: "no kubeconfig found to convert", + }, + { + name: "without outfile the source kubeconfig is converted in place", + sources: [][2]string{{"aks1", "aks2"}}, + explicitKubeconfig: true, + }, + { + name: "without outfile merged KUBECONFIG files are converted in place", + sources: [][2]string{{"aks1", "aks2"}, {"aks3", "aks4"}}, + }, + } + + for _, data := range testData { + t.Run(data.name, func(t *testing.T) { + tmpDir := t.TempDir() + + var sourceFiles []string + before := map[string][]byte{} + for i, names := range data.sources { + config := createValidTestConfigs(names[0], names[1], "", azureAuthProvider, authProviderConfig, nil, "") + for _, cluster := range config.Clusters { + // relative to the source file, so the outfile must not keep it as is + cluster.CertificateAuthority = caFile + } + file := filepath.Join(tmpDir, fmt.Sprintf("source-%d", i), "config") + if err := clientcmd.WriteToFile(*config, file); err != nil { + t.Fatalf("unable to write source kubeconfig: %s", err) + } + content, err := os.ReadFile(file) + if err != nil { + t.Fatalf("unable to read source kubeconfig: %s", err) + } + sourceFiles = append(sourceFiles, file) + before[file] = content + } + kubeconfigEnv := strings.Join(sourceFiles, string(os.PathListSeparator)) + if data.missingKubeconfig { + kubeconfigEnv = filepath.Join(tmpDir, "missing", "config") + } + t.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfigEnv) + + o := New() + fs := &pflag.FlagSet{} + o.Flags = fs + o.AddFlags(fs) + overrideFlags := map[string]string{} + if data.explicitKubeconfig { + overrideFlags["kubeconfig"] = sourceFiles[0] + } + outFile := "" + if data.outFile != "" { + outFile = filepath.Join(tmpDir, data.outFile) + switch data.alias { + case "symlink": + if err := os.Symlink(sourceFiles[0], outFile); err != nil { + t.Skipf("symlinks are not supported: %s", err) + } + case "hardlink": + if err := os.Link(sourceFiles[0], outFile); err != nil { + t.Skipf("hard links are not supported: %s", err) + } + } + overrideFlags[flagOutFile] = outFile + } + if data.context != "" { + overrideFlags[flagContext] = data.context + } + for k, v := range overrideFlags { + if err := o.setFlag(k, v); err != nil { + t.Fatalf("unable to add flag: %s, err: %s", k, err) + } + } + + // same wiring as the convert-kubeconfig command + pathOptions := clientcmd.NewDefaultPathOptions() + pathOptions.LoadingRules.ExplicitPath, _ = o.Flags.GetString("kubeconfig") + + err := Convert(o, pathOptions) + if data.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), data.expectedError) { + t.Fatalf("expected error containing %q, got: %v", data.expectedError, err) + } + } else if err != nil { + t.Fatalf("unexpected error from Convert: %v", err) + } + + if outFile == "" { + // old behaviour: every source file is converted in place + for i, file := range sourceFiles { + got, err := clientcmd.LoadFromFile(file) + if err != nil { + t.Fatalf("unable to load source kubeconfig: %s", err) + } + for _, name := range data.sources[i] { + validate(t, name, got.AuthInfos[name], expectedArgs, "", "", nil) + } + } + return + } + + for _, file := range sourceFiles { + after, err := os.ReadFile(file) + if err != nil { + t.Fatalf("unable to read source kubeconfig: %s", err) + } + if !bytes.Equal(before[file], after) { + t.Fatalf("source kubeconfig %s was modified:\nbefore:\n%s\nafter:\n%s", file, before[file], after) + } + } + + if data.expectedError != "" { + if data.missingKubeconfig { + if _, err := os.Stat(outFile); !os.IsNotExist(err) { + t.Fatalf("expected no outfile to be written, got: %v", err) + } + } + return + } + + info, err := os.Stat(outFile) + if err != nil { + t.Fatalf("outfile was not written: %s", err) + } + if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 { + t.Fatalf("expected outfile mode 0600, got %o", info.Mode().Perm()) + } + + got, err := clientcmd.LoadFromFile(outFile) + if err != nil { + t.Fatalf("unable to load outfile: %s", err) + } + if got.CurrentContext != data.sources[0][0] { + t.Fatalf("expected current context %q, got %q", data.sources[0][0], got.CurrentContext) + } + total := 0 + for i, names := range data.sources { + for _, name := range names { + total++ + if data.context != "" && name != data.context { + validateAuthInfoThatShouldNotChange(t, name, got.AuthInfos[name], authProviderConfig) + } else { + validate(t, name, got.AuthInfos[name], expectedArgs, "", "", nil) + } + if got.Contexts[name] == nil || got.Clusters[name] == nil { + t.Fatalf("[context:%s]: context or cluster missing from outfile", name) + } + expectedCA := filepath.Join(filepath.Dir(sourceFiles[i]), caFile) + if got.Clusters[name].CertificateAuthority != expectedCA { + t.Fatalf("[context:%s]: expected certificate-authority %q, got %q", name, expectedCA, got.Clusters[name].CertificateAuthority) + } + } + } + if len(got.AuthInfos) != total || len(got.Contexts) != total || len(got.Clusters) != total { + t.Fatalf("expected %d users, contexts and clusters in outfile, got %d, %d, %d", total, len(got.AuthInfos), len(got.Contexts), len(got.Clusters)) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.21/pkg/internal/converter/options.go new/kubelogin-0.2.22/pkg/internal/converter/options.go --- old/kubelogin-0.2.21/pkg/internal/converter/options.go 2026-10-08 23:03:33.000000000 +0200 +++ new/kubelogin-0.2.22/pkg/internal/converter/options.go 2026-10-24 06:18:44.000000000 +0200 @@ -16,6 +16,9 @@ // context is the kubeconfig context name context string azureConfigDir string + // outFile is the path the converted kubeconfig is written to; when empty, + // the source kubeconfig is modified in place + outFile string } func stringptr(str string) *string { return &str } @@ -34,6 +37,7 @@ } fs.StringVar(&o.context, flagContext, "", "The name of the kubeconfig context to use") fs.StringVar(&o.azureConfigDir, flagAzureConfigDir, "", "Azure CLI config path") + fs.StringVar(&o.outFile, flagOutFile, "", "Write the converted kubeconfig to this file instead of modifying the source kubeconfig") o.TokenOptions.AddFlags(fs) } @@ -63,6 +67,7 @@ _ = cmd.RegisterFlagCompletionFunc(flagContext, completeContexts(o)) _ = cmd.MarkFlagDirname(flagAzureConfigDir) _ = cmd.MarkFlagFilename("kubeconfig", "") + _ = cmd.MarkFlagFilename(flagOutFile, "") o.TokenOptions.AddCompletions(cmd) ++++++ kubelogin.obsinfo ++++++ --- /var/tmp/diff_new_pack.ZkyBeN/_old 2026-10-27 17:08:20.781188716 +0100 +++ /var/tmp/diff_new_pack.ZkyBeN/_new 2026-10-27 17:08:20.793189218 +0100 @@ -1,5 +1,5 @@ name: kubelogin -version: 0.2.21 -mtime: 1791493413 -commit: 532826311d3a5458df998a2cf910f5dec2a66e17 +version: 0.2.22 +mtime: 1792815524 +commit: f341e3167eb2c077fd95f73b3c454236262299ce ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/kubelogin/vendor.tar.gz /work/SRC/openSUSE:Factory/.kubelogin.new.2252680/vendor.tar.gz differ: char 87, line 1
