Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop importing the k8s.io/kubernetes module. #6411

Merged
merged 7 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cmd/asset-syncer/server/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
httpclient "github.com/vmware-tanzu/kubeapps/pkg/http-client"
"github.com/vmware-tanzu/kubeapps/pkg/kube"
log "k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/credentialprovider"
)

func Sync(serveOpts Config, version string, args []string) error {
Expand Down Expand Up @@ -41,7 +40,7 @@ func Sync(serveOpts Config, version string, args []string) error {
authorizationHeader := serveOpts.AuthorizationHeader
// The auth header may be a dockerconfig that we need to parse
if serveOpts.DockerConfigJson != "" {
dockerConfig := &credentialprovider.DockerConfigJSON{}
dockerConfig := &kube.DockerConfigJSON{}
err = json.Unmarshal([]byte(serveOpts.DockerConfigJson), dockerConfig)
if err != nil {
return fmt.Errorf("Error: %v", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
corev1 "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
"github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/pkg/connecterror"
"github.com/vmware-tanzu/kubeapps/pkg/kube"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
log "k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/credentialprovider"
)

const (
Expand Down Expand Up @@ -592,8 +592,8 @@ func newLocalOpaqueSecret(ownerRepo types.NamespacedName) *apiv1.Secret {
}

func encodeDockerAuth(credentials *corev1.DockerCredentials) ([]byte, error) {
config := &credentialprovider.DockerConfigJSON{
Auths: map[string]credentialprovider.DockerConfigEntry{
config := &kube.DockerConfigJSON{
Auths: map[string]kube.DockerConfigEntry{
credentials.Server: {
Username: credentials.Username,
Password: credentials.Password,
Expand All @@ -605,7 +605,7 @@ func encodeDockerAuth(credentials *corev1.DockerCredentials) ([]byte, error) {
}

func decodeDockerAuth(dockerjson []byte) (*corev1.DockerCredentials, error) {
config := &credentialprovider.DockerConfigJSON{}
config := &kube.DockerConfigJSON{}
if err := json.Unmarshal(dockerjson, config); err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/bufbuild/connect-go"
"github.com/vmware-tanzu/kubeapps/pkg/helm"
"github.com/vmware-tanzu/kubeapps/pkg/kube"

apprepov1alpha1 "github.com/vmware-tanzu/kubeapps/cmd/apprepository-controller/pkg/apis/apprepository/v1alpha1"
corev1 "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
Expand All @@ -24,7 +25,6 @@ import (
"k8s.io/client-go/kubernetes"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
log "k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/credentialprovider"
)

const (
Expand Down Expand Up @@ -844,8 +844,8 @@ func decodeBearerAuth(auth string) (token string, ok bool) {
}

func encodeDockerAuth(credentials *corev1.DockerCredentials) ([]byte, error) {
config := &credentialprovider.DockerConfigJSON{
Auths: map[string]credentialprovider.DockerConfigEntry{
config := &kube.DockerConfigJSON{
Auths: map[string]kube.DockerConfigEntry{
credentials.Server: {
Username: credentials.Username,
Password: credentials.Password,
Expand All @@ -857,7 +857,7 @@ func encodeDockerAuth(credentials *corev1.DockerCredentials) ([]byte, error) {
}

func decodeDockerAuth(dockerjson []byte) (*corev1.DockerCredentials, error) {
config := &credentialprovider.DockerConfigJSON{}
config := &kube.DockerConfigJSON{}
if err := json.Unmarshal(dockerjson, config); err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (
"context"
"encoding/json"
"fmt"

"github.com/vmware-tanzu/kubeapps/pkg/chart/models"
"github.com/vmware-tanzu/kubeapps/pkg/dbutils"
"github.com/vmware-tanzu/kubeapps/pkg/kube"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/credentialprovider"
)

const (
Expand Down Expand Up @@ -64,7 +65,7 @@ func RegistrySecretsPerDomain(ctx context.Context, appRepoSecrets []string, name
return nil, fmt.Errorf("AppRepository secret must have a data map with a key %q. Secret %q did not", dockerConfigJSONKey, secretName)
}

dockerConfigJSON := credentialprovider.DockerConfigJSON{}
dockerConfigJSON := kube.DockerConfigJSON{}
if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/credentialprovider"

k8scorev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -26,6 +25,7 @@ import (
corev1 "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
kappcorev1 "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1"
"github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/pkg/pkgutils"
"github.com/vmware-tanzu/kubeapps/pkg/kube"
)

const REPO_REF_ANNOTATION = "packaging.carvel.dev/package-repository-ref"
Expand Down Expand Up @@ -534,8 +534,8 @@ func isDockerAuth(secret *k8scorev1.Secret) bool {
}

func toDockerConfig(docker *corev1.DockerCredentials) ([]byte, error) {
dockerConfig := &credentialprovider.DockerConfigJSON{
Auths: map[string]credentialprovider.DockerConfigEntry{
dockerConfig := &kube.DockerConfigJSON{
Auths: map[string]kube.DockerConfigEntry{
docker.Server: {
Username: docker.Username,
Password: docker.Password,
Expand All @@ -551,7 +551,7 @@ func toDockerConfig(docker *corev1.DockerCredentials) ([]byte, error) {
}

func fromDockerConfig(dockerjson []byte) (*corev1.DockerCredentials, error) {
dockerConfig := &credentialprovider.DockerConfigJSON{}
dockerConfig := &kube.DockerConfigJSON{}
if err := json.Unmarshal(dockerjson, dockerConfig); err != nil {
return nil, err
}
Expand Down
37 changes: 2 additions & 35 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,6 @@ module github.com/vmware-tanzu/kubeapps

go 1.19

replace (
// k8s.io/kubernetes is not intended to be used as a module, so versions are not being properly resolved.
// This replacement is requiredƒ, see https://github.com/kubernetes/kubernetes/issues/79384
// As we support new k8s versions, this replacements should be also updated accordingly.
k8s.io/api => k8s.io/api v0.26.1
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.26.1
k8s.io/apimachinery => k8s.io/apimachinery v0.26.1
k8s.io/apiserver => k8s.io/apiserver v0.26.1
k8s.io/cli-runtime => k8s.io/cli-runtime v0.26.1
k8s.io/client-go => k8s.io/client-go v0.26.1
k8s.io/cloud-provider => k8s.io/cloud-provider v0.26.1
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.26.1
k8s.io/code-generator => k8s.io/code-generator v0.26.1
k8s.io/component-base => k8s.io/component-base v0.26.1
k8s.io/component-helpers => k8s.io/component-helpers v0.26.1
k8s.io/controller-manager => k8s.io/controller-manager v0.26.1
k8s.io/cri-api => k8s.io/cri-api v0.26.1
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.26.1
k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.26.1
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.26.1
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.26.1
k8s.io/kube-proxy => k8s.io/kube-proxy v0.26.1
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.26.1
k8s.io/kubectl => k8s.io/kubectl v0.26.1
k8s.io/kubelet => k8s.io/kubelet v0.26.1
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.26.1
k8s.io/metrics => k8s.io/metrics v0.26.1
k8s.io/mount-utils => k8s.io/mount-utils v0.26.1
k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.26.1
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.26.1
)

require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver/v3 v3.2.1
Expand Down Expand Up @@ -86,12 +54,11 @@ require (
k8s.io/api v0.27.2
k8s.io/apiextensions-apiserver v0.26.3
k8s.io/apimachinery v0.27.3
k8s.io/apiserver v0.26.2
k8s.io/apiserver v0.26.3
k8s.io/cli-runtime v0.26.1
k8s.io/client-go v0.27.2
k8s.io/klog/v2 v2.100.1
k8s.io/kubectl v0.26.1
k8s.io/kubernetes v1.26.5
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5
oras.land/oras-go v1.2.3
oras.land/oras-go/v2 v2.2.0
Expand Down Expand Up @@ -254,7 +221,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.26.2 // indirect
k8s.io/component-base v0.26.3 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1464,18 +1464,30 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ=
k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg=
k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo=
k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4=
k8s.io/apiextensions-apiserver v0.26.1 h1:cB8h1SRk6e/+i3NOrQgSFij1B2S0Y0wDoNl66bn8RMI=
k8s.io/apiextensions-apiserver v0.26.1/go.mod h1:AptjOSXDGuE0JICx/Em15PaoO7buLwTs0dGleIHixSM=
k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE=
k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ=
k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
k8s.io/apiserver v0.26.1 h1:6vmnAqCDO194SVCPU3MU8NcDgSqsUA62tBUSWrFXhsc=
k8s.io/apiserver v0.26.1/go.mod h1:wr75z634Cv+sifswE9HlAo5FQ7UoUauIICRlOE+5dCg=
k8s.io/apiserver v0.26.3 h1:blBpv+yOiozkPH2aqClhJmJY+rp53Tgfac4SKPDJnU4=
k8s.io/apiserver v0.26.3/go.mod h1:CJe/VoQNcXdhm67EvaVjYXxR3QyfwpceKPuPaeLibTA=
k8s.io/cli-runtime v0.26.1 h1:f9+bRQ1V3elQsx37KmZy5fRAh56mVLbE9A7EMdlqVdI=
k8s.io/cli-runtime v0.26.1/go.mod h1:+e5Ym/ARySKscUhZ8K3hZ+ZBo/wYPIcg+7b5sFYi6Gg=
k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE=
k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ=
k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4=
k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU=
k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g=
k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
Expand Down
116 changes: 116 additions & 0 deletions pkg/kube/dockerconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2019-2022 the Kubeapps contributors.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2019-2022 the Kubeapps contributors.
// Copyright 2023 the Kubeapps contributors.

Also, but unrelated, I think we should update the NOTICE file and remove this old file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I've been removing the old OSL license files later (I don't think we personally need to add them to the root of the repo anyway, given that we upload them to the release artifacts and leave them there.. wdyt?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we personally need to add them to the root of the repo

The old ones? No, I don't think so. As for the current one (the one to be released), we need it just before tagging the release, this way it would end up in the release artifact.
Right after that... I believe we can safely remove it; in fact, that file no longer represents the current status of the project, as we might add/remove deps and, moreover, it is just a document to be embedded in a release artifact.

That said... I think it's easier for us to just remove it as part of the release process: remove the old one and add the new one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right after that... I believe we can safely remove it; in fact, that file no longer represents the current status of the project, as we might add/remove deps and, moreover, it is just a document to be embedded in a release artifact.

Which is why I think it's more useful uploading it as a (separate) release artifact for that release, rather than temporarily including it in the repo. But IANAL.

// SPDX-License-Identifier: Apache-2.0
package kube

import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
)

// The following is Copyright 2014 The Kubernetes Authors
// and icensed under the Apache License, Version 2.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we, perhaps, need to explicitly use the same copyright text, I was thinking sth more like:

Also, should we add the permalink instead of the link pointing to master? Not sure :S

// Including here instead of importing from k8s.io/kubernetes/credentialprovider
// since k8s.io/kubernetes is not supported for imports and leads to version issues.

// the following pieces of code have been extracted from 
// https://github.com/kubernetes/kubernetes/blob/master/pkg/credentialprovider/provider.go
// and they are subject to the undermentioned license terms.

/*
Copyright 2014 The Kubernetes 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.
*/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, will update.

// Including here instead of importing from k8s.io/kubernetes/credentialprovider
// since k8s.io/kubernetes is not supported for imports and leads
// to version issues.

// DockerConfigProvider is the interface that registered extensions implement
// to materialize 'dockercfg' credentials.
type DockerConfigProvider interface {
// Enabled returns true if the config provider is enabled.
// Implementations can be blocking - e.g. metadata server unavailable.
Enabled() bool
// Provide returns docker configuration.
// Implementations can be blocking - e.g. metadata server unavailable.
// The image is passed in as context in the event that the
// implementation depends on information in the image name to return
// credentials; implementations are safe to ignore the image.
Provide(image string) DockerConfig
}

// DockerConfigJSON represents ~/.docker/config.json file info
// see https://github.com/docker/docker/pull/12009
type DockerConfigJSON struct {
Auths DockerConfig `json:"auths"`
// +optional
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
}

// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type DockerConfig map[string]DockerConfigEntry

// DockerConfigEntry wraps a docker config as a entry
type DockerConfigEntry struct {
Username string
Password string
Email string
Provider DockerConfigProvider
}

// dockerConfigEntryWithAuth is used solely for deserializing the Auth field
// into a dockerConfigEntry during JSON deserialization.
type dockerConfigEntryWithAuth struct {
// +optional
Username string `json:"username,omitempty"`
// +optional
Password string `json:"password,omitempty"`
// +optional
Email string `json:"email,omitempty"`
// +optional
Auth string `json:"auth,omitempty"`
}

func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
var tmp dockerConfigEntryWithAuth
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}

ident.Username = tmp.Username
ident.Password = tmp.Password
ident.Email = tmp.Email

if len(tmp.Auth) == 0 {
return nil
}

ident.Username, ident.Password, err = decodeDockerConfigFieldAuth(tmp.Auth)
return err
}

func (ident DockerConfigEntry) MarshalJSON() ([]byte, error) {
toEncode := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""}
toEncode.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password)

return json.Marshal(toEncode)
}

// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a
// username and a password. The format of the auth field is base64(<username>:<password>).
func decodeDockerConfigFieldAuth(field string) (username, password string, err error) {
decoded, err := base64.StdEncoding.DecodeString(field)
if err != nil {
return
}

parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
err = fmt.Errorf("unable to parse auth field")
return
}

username = parts[0]
password = parts[1]

return
}

func encodeDockerConfigFieldAuth(username, password string) string {
fieldValue := username + ":" + password

return base64.StdEncoding.EncodeToString([]byte(fieldValue))
}
6 changes: 3 additions & 3 deletions pkg/kube/kube_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"encoding/base64"
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/credentialprovider"
)

func GetAuthHeaderFromDockerConfig(dockerConfig *credentialprovider.DockerConfigJSON) (string, error) {
func GetAuthHeaderFromDockerConfig(dockerConfig *DockerConfigJSON) (string, error) {
if len(dockerConfig.Auths) > 1 {
return "", fmt.Errorf("the given config should include one auth entry")
}
Expand All @@ -32,7 +32,7 @@ func getDataFromRegistrySecret(key string, s *corev1.Secret) (string, error) {
return "", fmt.Errorf("secret %q did not contain key %q", s.Name, key)
}

dockerConfig := &credentialprovider.DockerConfigJSON{}
dockerConfig := &DockerConfigJSON{}
err := json.Unmarshal(dockerConfigJson, dockerConfig)
if err != nil {
return "", fmt.Errorf("unable to parse secret %s as a Docker config. Got: %v", s.Name, err)
Expand Down