Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
5e3ab732df [Backport release-1.4] Feat: add the API for querying the image info (#4211)
* Feat: add the API for querying the image info

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit e257cc6658)

* Fix: the code style

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 45e183b44b)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-06-21 15:05:15 +08:00
github-actions[bot]
62d5507499 Fix: cue patch remove temp var (#4208)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 660e89b3a0)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-06-20 17:38:34 +08:00
github-actions[bot]
c0daf688a6 Fix: clear namespace for cluster scoped resource for dispatching (#4195)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 7c9de1b071)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-06-16 14:50:44 +08:00
github-actions[bot]
48d19a2427 [Backport release-1.4] Feat: cli addon add registry add more git types (#4191)
* add more git types

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 1d6a5eea22)

* fix comments

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix comments

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 91122432ec)

Co-authored-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-06-16 12:23:15 +08:00
github-actions[bot]
4da8d49e60 Fix: fix the annotation for APIService (#4190)
Make the annatation for cert-manger be the same as the new version
secret

Signed-off-by: Zheng Xi Zhou <zhengxi.zzx@alibaba-inc.com>
(cherry picked from commit 62ef7b7f99)

Co-authored-by: Zheng Xi Zhou <zhengxi.zzx@alibaba-inc.com>
2022-06-16 11:02:35 +08:00
github-actions[bot]
4db9e89816 Fix: enhance CLI for managing OCM clusters (#4178)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 0662e2a79d)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-06-15 11:23:12 +08:00
github-actions[bot]
667053409d Fix: fix trait customStatus error when controlPlanOnly=true (#4177)
Signed-off-by: fourierr <maxiangboo@qq.com>
(cherry picked from commit 38648fcf30)

Co-authored-by: fourierr <maxiangboo@qq.com>
2022-06-15 11:22:15 +08:00
34 changed files with 737 additions and 733 deletions

View File

@@ -106,7 +106,7 @@ metadata:
name: v1alpha1.cluster.core.oam.dev
annotations:
{{- if and .Values.multicluster.clusterGateway.secureTLS.enabled .Values.multicluster.clusterGateway.secureTLS.certManager.enabled }}
cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ template "kubevela.fullname" . }}-cluster-gateway-tls"
cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ template "kubevela.fullname" . }}-cluster-gateway-tls-v2"
{{- end }}
labels:
api: cluster-extension-apiserver

View File

@@ -20,6 +20,7 @@ spec:
import (
"encoding/base64"
"encoding/json"
"strconv"
)
output: {
@@ -42,21 +43,29 @@ spec:
if parameter.auth == _|_ {
type: "Opaque"
}
if parameter.auth != _|_ {
stringData: ".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
stringData: {
if parameter.auth != _|_ && parameter.auth.username != _|_ {
".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
})
})
}
if parameter.insecure != _|_ {
"insecure-skip-verify": strconv.FormatBool(parameter.insecure)
}
if parameter.useHTTP != _|_ {
"protocol-use-http": strconv.FormatBool(parameter.useHTTP)
}
}
}
parameter: {
// +usage=Image registry FQDN
// +usage=Image registry FQDN, such as: index.docker.io
registry: string
// +usage=Authenticate the image registry
auth?: {
@@ -67,6 +76,10 @@ spec:
// +usage=Private Image registry email
email?: string
}
// +usage=For the registry server that uses the self-signed certificate
insecure?: bool
// +usage=For the registry server that uses the HTTP protocol
useHTTP?: bool
}
workload:
type: autodetects.core.oam.dev

View File

@@ -20,6 +20,7 @@ spec:
import (
"encoding/base64"
"encoding/json"
"strconv"
)
output: {
@@ -42,21 +43,29 @@ spec:
if parameter.auth == _|_ {
type: "Opaque"
}
if parameter.auth != _|_ {
stringData: ".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
stringData: {
if parameter.auth != _|_ && parameter.auth.username != _|_ {
".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
})
})
}
if parameter.insecure != _|_ {
"insecure-skip-verify": strconv.FormatBool(parameter.insecure)
}
if parameter.useHTTP != _|_ {
"protocol-use-http": strconv.FormatBool(parameter.useHTTP)
}
}
}
parameter: {
// +usage=Image registry FQDN
// +usage=Image registry FQDN, such as: index.docker.io
registry: string
// +usage=Authenticate the image registry
auth?: {
@@ -67,6 +76,10 @@ spec:
// +usage=Private Image registry email
email?: string
}
// +usage=For the registry server that uses the self-signed certificate
insecure?: bool
// +usage=For the registry server that uses the HTTP protocol
useHTTP?: bool
}
workload:
type: autodetects.core.oam.dev

2
go.mod
View File

@@ -46,7 +46,7 @@ require (
github.com/hashicorp/hcl/v2 v2.9.1
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
github.com/imdario/mergo v0.3.12
github.com/kubevela/prism v1.4.0
github.com/kubevela/prism v1.4.1-0.20220613123457-94f1190f87c2
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/mitchellh/hashstructure/v2 v2.0.1
github.com/oam-dev/cluster-gateway v1.4.0

4
go.sum
View File

@@ -1344,8 +1344,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubevela/prism v1.4.0 h1:wYCKXA3p9YpkcSsZjGnSEGBVL+3bPoZNEt4DYs3IxW4=
github.com/kubevela/prism v1.4.0/go.mod h1:RP69+bRb57Occer6BeeF5zK3hrD1IhnYf2RNRsIdh9E=
github.com/kubevela/prism v1.4.1-0.20220613123457-94f1190f87c2 h1:TaHlO4raKI3ehVSYY8QixYMHdI0VwKHY1KPNWcUre3I=
github.com/kubevela/prism v1.4.1-0.20220613123457-94f1190f87c2/go.mod h1:RP69+bRb57Occer6BeeF5zK3hrD1IhnYf2RNRsIdh9E=
github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U=
github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30=
github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4=

View File

@@ -29,6 +29,12 @@ func init() {
RegisterModel(&WorkflowRecord{})
}
// Finished means the workflow record is finished
const Finished = "true"
// UnFinished means the workflow record is not finished
const UnFinished = "false"
// Workflow application delivery database model
type Workflow struct {
BaseModel

View File

@@ -507,14 +507,14 @@ func (c *applicationServiceImpl) UpdateApplication(ctx context.Context, app *mod
func (c *applicationServiceImpl) ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error) {
var record = model.WorkflowRecord{
AppPrimaryKey: appName,
Finished: "false",
Finished: model.UnFinished,
}
records, err := c.Store.List(ctx, &record, &datastore.ListOptions{})
if err != nil {
return nil, err
}
if len(records) == 0 {
record.Finished = "true"
record.Finished = model.Finished
records, err = c.Store.List(ctx, &record, &datastore.ListOptions{
Page: 1,
PageSize: 1,

View File

@@ -60,7 +60,10 @@ var _ = Describe("Test cluster service function", func() {
secret.Name = name
secret.Namespace = prismclusterv1alpha1.StorageNamespace
secret.SetAnnotations(map[string]string{prismclusterv1alpha1.AnnotationClusterAlias: alias})
secret.SetLabels(map[string]string{clustergatewaycommon.LabelKeyClusterCredentialType: string(clustergatewayv1alpha1.CredentialTypeX509Certificate)})
secret.SetLabels(map[string]string{
clustergatewaycommon.LabelKeyClusterEndpointType: string(clustergatewayv1alpha1.ClusterEndpointTypeConst),
clustergatewaycommon.LabelKeyClusterCredentialType: string(clustergatewayv1alpha1.CredentialTypeX509Certificate),
})
time.Sleep(time.Second)
return k8sClient.Create(ctx, secret)
}

View File

@@ -52,6 +52,9 @@ type DefinitionService interface {
UpdateDefinitionStatus(ctx context.Context, name string, status apisv1.UpdateDefinitionStatusRequest) (*apisv1.DetailDefinitionResponse, error)
}
// DefinitionHidden means the definition can not be used in VelaUX
const DefinitionHidden = "true"
type definitionServiceImpl struct {
KubeClient client.Client `inject:"kubeClient"`
}
@@ -344,7 +347,7 @@ func (d *definitionServiceImpl) UpdateDefinitionStatus(ctx context.Context, name
}
if !exist && update.HiddenInUI {
labels := def.GetLabels()
labels[types.LabelDefinitionHidden] = "true"
labels[types.LabelDefinitionHidden] = DefinitionHidden
def.SetLabels(labels)
if err := d.KubeClient.Update(ctx, def); err != nil {
return nil, err

View File

@@ -0,0 +1,228 @@
/*
Copyright 2022 The KubeVela 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 service
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/types"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
)
// NewImageService create a image service instance
func NewImageService() ImageService {
return &imageImpl{}
}
// ImageService the image service provide some handler functions about the docker image
type ImageService interface {
ListImageRepos(ctx context.Context, project string) ([]v1.ImageRegistry, error)
GetImageInfo(ctx context.Context, project, secretName, imageName string) v1.ImageInfo
}
type imageImpl struct {
K8sClient client.Client `inject:"kubeClient"`
}
// ListImageRepos list the image repositories via user configuration
func (i *imageImpl) ListImageRepos(ctx context.Context, project string) ([]v1.ImageRegistry, error) {
var secrets corev1.SecretList
if err := i.K8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: types.ImageRegistry,
}); err != nil {
return nil, err
}
var repos []v1.ImageRegistry
for _, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
repos = append(repos, v1.ImageRegistry{
Name: secret.Name,
SecretName: secret.Name,
Domain: secret.Labels[types.LabelConfigIdentifier],
})
}
}
return repos, nil
}
// GetImageInfo get the image info from image registry
func (i *imageImpl) GetImageInfo(ctx context.Context, project, secretName, imageName string) v1.ImageInfo {
var imageInfo = v1.ImageInfo{
Name: imageName,
}
ref, err := name.ParseReference(imageName)
if err != nil {
imageInfo.Message = "The image name is invalid"
return imageInfo
}
registryDomain := ref.Context().RegistryStr()
imageInfo.Registry = registryDomain
var secrets corev1.SecretList
if err := i.K8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: types.ImageRegistry,
types.LabelConfigIdentifier: registryDomain,
}); err != nil {
log.Logger.Warnf("fail to list the docker registries, %s", err.Error())
}
var selectSecret []*corev1.Secret
var selectSecretNames []string
// get info with specified secret
if secretName != "" {
for i, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
if secretName == secret.Name {
selectSecret = append(selectSecret, &secrets.Items[i])
selectSecretNames = append(selectSecretNames, secret.Name)
break
}
}
}
}
// get info with the secret which match the registry domain
if selectSecret == nil {
for i, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
if secret.Labels[types.LabelConfigIdentifier] == registryDomain {
selectSecret = append(selectSecret, &secrets.Items[i])
selectSecretNames = append(selectSecretNames, secret.Name)
}
}
}
}
var username, password string
var insecure = false
var useHTTP = false
imageInfo.SecretNames = selectSecretNames
if len(selectSecret) > 0 {
insecure, useHTTP, username, password = getAccountFromSecret(*selectSecret[0], registryDomain)
}
err = getImageInfo(imageName, insecure, useHTTP, username, password, &imageInfo)
if err != nil {
imageInfo.Message = fmt.Sprintf("Fail to get the image info:%s", err.Error())
}
return imageInfo
}
// getAccountFromSecret get the username and password from the secret of `kubernetes.io/dockerconfigjson` type
// refer: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
func getAccountFromSecret(secret corev1.Secret, registryDomain string) (insecure, useHTTP bool, username, password string) {
if secret.Data != nil {
// If users use the self-signed certificate, enable the insecure-skip-verify
insecure = string(secret.Data["insecure-skip-verify"]) == "true"
useHTTP = string(secret.Data["protocol-use-http"]) == "true"
conf := secret.Data[".dockerconfigjson"]
if len(conf) > 0 {
var authConfig map[string]map[string]map[string]string
if err := json.Unmarshal(conf, &authConfig); err != nil {
log.Logger.Warnf("fail to unmarshal the secret %s , %s", secret.Name, err.Error())
return
}
if authConfig != nil && authConfig["auths"] != nil && authConfig["auths"][registryDomain] != nil {
data := authConfig["auths"][registryDomain]
username = data["username"]
password = data["password"]
}
}
}
return
}
func getImageInfo(imageName string, insecure, useHTTP bool, username, password string, info *v1.ImageInfo) error {
var options []remote.Option
if username != "" || password != "" {
basic := &authn.Basic{
Username: username,
Password: password,
}
options = append(options, remote.WithAuth(basic))
}
if insecure {
options = append(options, remote.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
// By default we wrap the transport in retries, so reduce the
// default dial timeout to 5s to avoid 5x 30s of connection
// timeouts when doing the "ping" on certain http registries.
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// #nosec G402
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
}))
}
var parseOptions []name.Option
if useHTTP {
parseOptions = append(parseOptions, name.Insecure)
}
var err error
ref, err := name.ParseReference(imageName, parseOptions...)
if err != nil {
return err
}
image, err := remote.Image(ref, options...)
if err != nil {
if strings.Contains(err.Error(), "incorrect username or password") {
return fmt.Errorf("incorrect username or password")
}
var terr *transport.Error
if errors.As(err, &terr) {
fmt.Println(terr)
}
return err
}
info.Manifest, err = image.Manifest()
if err != nil {
return fmt.Errorf("fail to get the manifest:%w", err)
}
info.Info, err = image.ConfigFile()
if err != nil {
return fmt.Errorf("fail to get the config:%w", err)
}
for _, l := range info.Manifest.Layers {
info.Size += l.Size
}
info.Size += info.Manifest.Config.Size
return nil
}

View File

@@ -0,0 +1,67 @@
/*
Copyright 2022 The KubeVela 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 service
import (
"testing"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
velatypes "github.com/oam-dev/kubevela/apis/types"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
)
func TestGetImageInfo(t *testing.T) {
s2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s2",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: velatypes.ImageRegistry,
velatypes.LabelConfigProject: "",
velatypes.LabelConfigIdentifier: "index.docker.io",
},
},
Data: map[string][]byte{
"insecure-skip-verify": []byte("true"),
".dockerconfigjson": []byte(`{"auths":{"index.docker.io":{"auth":"aHlicmlkY2xvdWRAcHJvZC5YTEyMw==","username":"xxx","password":"yyy"}}}`),
},
}
insecure, useHTTP, user, pass := getAccountFromSecret(*s2, "index.docker.io")
assert.DeepEqual(t, user, "xxx")
assert.DeepEqual(t, pass, "yyy")
assert.DeepEqual(t, insecure, true)
assert.DeepEqual(t, useHTTP, false)
var cf v1.ImageInfo
// Test the public image
err := getImageInfo("nginx", false, false, "", "", &cf)
assert.DeepEqual(t, err, nil)
assert.DeepEqual(t, cf.Info.Config.Entrypoint, []string{"/docker-entrypoint.sh"})
// Test the private image
err = getImageInfo("nginx424ru823-should-not-existed", false, false, "abc", "efg", &cf)
assert.DeepEqual(t, err.Error(), "incorrect username or password")
err = getImageInfo("text.registry/test-image", false, false, "", "", &cf)
assert.DeepEqual(t, err != nil, true)
}

View File

@@ -18,15 +18,12 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/google/go-containerregistry/pkg/name"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -41,7 +38,6 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
"github.com/oam-dev/kubevela/pkg/multicluster"
image "github.com/oam-dev/kubevela/pkg/utils/imageregistry"
)
// ProjectService project manage service.
@@ -59,7 +55,6 @@ type ProjectService interface {
UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error)
Init(ctx context.Context) error
GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error)
ValidateImage(ctx context.Context, projectName, image string) (*apisv1.ImageResponse, error)
}
type projectServiceImpl struct {
@@ -623,57 +618,3 @@ func retrieveConfigFromApplication(a v1beta1.Application, project string) *apisv
Description: a.Annotations[types.AnnotationConfigDescription],
}
}
func (p *projectServiceImpl) ValidateImage(ctx context.Context, projectName, image string) (*apisv1.ImageResponse, error) {
return validateImage(ctx, p.K8sClient, projectName, image)
}
func validateImage(ctx context.Context, k8sClient client.Client, project, imageName string) (*apisv1.ImageResponse, error) {
var (
secrets v1.SecretList
username string
password string
imagePullSecret string
)
ref, err := name.ParseReference(imageName)
if err != nil {
return nil, err
}
imageURL := ref.Context().RegistryStr()
if err := k8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: types.ImageRegistry,
types.LabelConfigIdentifier: ref.Context().RegistryStr(),
}); err != nil {
return nil, err
}
for _, s := range secrets.Items {
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
conf := s.Data[".dockerconfigjson"]
var auths map[string]map[string]map[string]string
if err := json.Unmarshal(conf, &auths); err != nil {
return nil, err
}
imagePullSecret = s.Name
if auths["auths"] != nil && auths["auths"][imageURL] != nil {
data := auths["auths"][imageURL]
username = data["username"]
password = data["password"]
break
}
}
}
existed, err := image.IsExisted(username, password, imageName)
if err != nil {
return nil, err
}
return &apisv1.ImageResponse{
Existed: existed,
Secret: imagePullSecret,
}, nil
}

View File

@@ -18,20 +18,14 @@ package service
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@@ -299,331 +293,3 @@ var _ = Describe("Test project service functions", func() {
Expect(roles.Total).Should(BeEquivalentTo(0))
})
})
func TestProjectGetConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
createdTime, _ := time.Parse(time.UnixDate, "Wed Apr 7 11:06:39 PST 2022")
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a1",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: "terraform-provider",
"config.oam.dev/project": "p1",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
app2 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a2",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: "terraform-provider",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
app3 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a3",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: "dex-connector",
"config.oam.dev/project": "p3",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
provider1 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider1",
Namespace: "default",
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsReady,
},
}
provider2 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider2",
Namespace: "default",
Labels: map[string]string{
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
},
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsNotReady,
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(app1, app2, app3, provider1, provider2).Build()
h := &projectServiceImpl{K8sClient: k8sClient}
type args struct {
projectName string
configType string
h ProjectService
}
type want struct {
configs []*apisv1.Config
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "project is matched",
args: args{
projectName: "p1",
configType: "terraform-provider",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a1",
Project: "p1",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "project is not matched",
args: args{
projectName: "p999",
configType: "terraform-provider",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "config type is empty",
args: args{
projectName: "p3",
configType: "",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
ConfigType: "dex-connector",
Name: "a3",
Project: "p3",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "config type is dex",
args: args{
projectName: "p3",
configType: "config-dex-connector",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "dex-connector",
Name: "a3",
Project: "p3",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}},
},
},
{
name: "config type is invalid",
args: args{
configType: "xxx",
h: h,
},
want: want{
errMsg: "unsupported config type",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigs(ctx, tc.args.projectName, tc.args.configType)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configs)
})
}
}
func TestValidateImage(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
s1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s1",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: velatypes.ImageRegistry,
velatypes.LabelConfigProject: "",
velatypes.LabelConfigIdentifier: "abce34289jwerojwerofaf77.com789",
},
},
Data: map[string][]byte{
".dockerconfigjson": []byte(`{"auths":{"abce34289jwerojwerofaf77.com789":{"auth":"aHlicmlkY2xvdWRAcHJvZC5YTEyMw==","username":"xxx","password":"yyy"}}}`),
},
}
k8sClient1 := fake.NewClientBuilder().WithScheme(s).WithObjects(s1).Build()
h1 := &projectServiceImpl{K8sClient: k8sClient1}
s2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s2",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
velatypes.LabelConfigCatalog: velatypes.VelaCoreConfig,
velatypes.LabelConfigType: velatypes.ImageRegistry,
velatypes.LabelConfigProject: "",
velatypes.LabelConfigIdentifier: "index.docker.io",
},
},
Data: map[string][]byte{
".dockerconfigjson": []byte(`{"auths":{"index.docker.io":{"auth":"aHlicmlkY2xvdWRAcHJvZC5YTEyMw==","username":"xxx","password":"yyy"}}}`),
},
}
k8sClient2 := fake.NewClientBuilder().WithScheme(s).WithObjects(s2).Build()
h2 := &projectServiceImpl{K8sClient: k8sClient2}
type args struct {
project string
imageName string
h ProjectService
}
type want struct {
resp *apisv1.ImageResponse
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "validate image",
args: args{
project: "p1",
imageName: "nginx",
h: h1,
},
want: want{
resp: &apisv1.ImageResponse{
Existed: true,
},
},
},
{
name: "invalid image",
args: args{
project: "p1",
imageName: "abce34289jwerojwerofaf77.com789/d/e:v1",
h: h1,
},
want: want{
errMsg: "Get",
},
},
{
name: "private docker image",
args: args{
project: "p1",
imageName: "nginx424ru823-should-not-existed",
h: h2,
},
want: want{
errMsg: "incorrect username or password",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.ValidateImage(ctx, tc.args.project, tc.args.imageName)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.resp)
})
}
}

View File

@@ -202,7 +202,6 @@ var ResourceMaps = map[string]resourceMetadata{
},
"applicationTemplate": {},
"config": {},
"image": {},
},
pathName: "projectName",
},

View File

@@ -50,7 +50,7 @@ func InitServiceBean(c config.Config) []interface{} {
return []interface{}{
clusterService, rbacService, projectService, envService, targetService, workflowService, oamApplicationService,
velaQLService, definitionService, addonService, envBindingService, systemInfoService, helmService, userService,
authenticationService, configService, applicationService, webhookService,
authenticationService, configService, applicationService, webhookService, NewImageService(),
}
}

View File

@@ -22,6 +22,7 @@ import (
"helm.sh/helm/v3/pkg/repo"
"github.com/getkin/kin-openapi/openapi3"
registryv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@@ -1380,3 +1381,26 @@ type ChartRepoResponse struct {
type ChartRepoResponseList struct {
ChartRepoResponse []*ChartRepoResponse `json:"repos"`
}
// ImageInfo the docker image info
type ImageInfo struct {
Name string `json:"name"`
SecretNames []string `json:"secretNames"`
Registry string `json:"registry"`
Message string `json:"message,omitempty"`
Info *registryv1.ConfigFile `json:"info,omitempty"`
Size int64 `json:"size"`
Manifest *registryv1.Manifest `json:"manifest"`
}
// ImageRegistry the image repository info
type ImageRegistry struct {
Name string `json:"name"`
SecretName string `json:"secretName"`
Domain string `json:"domain"`
}
// ListImageRegistryResponse the response struct of listing the image registries
type ListImageRegistryResponse struct {
Registries []ImageRegistry `json:"registries"`
}

View File

@@ -76,7 +76,7 @@ func InitAPIBean() []interface{} {
RegisterAPIInterface(NewTargetAPIInterface())
RegisterAPIInterface(NewVelaQLAPIInterface())
RegisterAPIInterface(NewWebhookAPIInterface())
RegisterAPIInterface(NewHelmAPIInterface())
RegisterAPIInterface(NewRepositoryAPIInterface())
// Authentication
RegisterAPIInterface(NewAuthenticationAPIInterface())

View File

@@ -203,16 +203,6 @@ func (n *projectAPIInterface) GetWebServiceRoute() *restful.WebService {
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]*apis.Config{}))
ws.Route(ws.GET("/{projectName}/validate_image").To(n.validateImage).
Doc("validate an image in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/image", "get")).
Param(ws.QueryParameter("image", "image name").DataType("string")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")).
Returns(200, "OK", []*apis.ImageResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]*apis.ImageResponse{}))
ws.Filter(authCheckFilter)
return ws
}
@@ -599,23 +589,3 @@ func (n *projectAPIInterface) getConfigs(req *restful.Request, res *restful.Resp
return
}
}
func (n *projectAPIInterface) validateImage(req *restful.Request, res *restful.Response) {
resp, err := n.ProjectService.ValidateImage(req.Request.Context(), req.PathParameter("projectName"), req.QueryParameter("image"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if resp == nil {
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
return
}
err = res.WriteEntity(map[string]interface{}{"data": resp})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package api
import (
"context"
"strconv"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
@@ -29,16 +28,18 @@ import (
"github.com/oam-dev/kubevela/pkg/utils"
)
type helmAPIInterface struct {
HelmService service.HelmService `inject:""`
type repositoryAPIInterface struct {
HelmService service.HelmService `inject:""`
ImageService service.ImageService `inject:""`
RbacService service.RBACService `inject:""`
}
// NewHelmAPIInterface will return helm APIInterface
func NewHelmAPIInterface() Interface {
return &helmAPIInterface{}
// NewRepositoryAPIInterface will return the repository APIInterface
func NewRepositoryAPIInterface() Interface {
return &repositoryAPIInterface{}
}
func (h helmAPIInterface) GetWebServiceRoute() *restful.WebService {
func (h repositoryAPIInterface) GetWebServiceRoute() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/repository").
Consumes(restful.MIME_XML, restful.MIME_JSON).
@@ -47,11 +48,12 @@ func (h helmAPIInterface) GetWebServiceRoute() *restful.WebService {
tags := []string{"repository", "helm"}
// List charts
// List chart repos
ws.Route(ws.GET("/chart_repos").To(h.listRepo).
Doc("list chart repo").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("project", "the config project").DataType("string")).
Param(ws.QueryParameter("project", "the config project").DataType("string").Required(true)).
Filter(h.RbacService.CheckPerm("project/config", "list")).
Returns(200, "OK", []string{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
@@ -86,11 +88,31 @@ func (h helmAPIInterface) GetWebServiceRoute() *restful.WebService {
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
ws.Route(ws.GET("/image/repos").To(h.getImageRepos).
Doc("get the oci repos").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("project", "the config project").DataType("string").Required(true)).
Filter(h.RbacService.CheckPerm("project/config", "list")).
Returns(200, "OK", v1.ListImageRegistryResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
ws.Route(ws.GET("/image/info").To(h.getImageInfo).
Doc("get the oci repos").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("project", "the config project").DataType("string").Required(true)).
Param(ws.QueryParameter("name", "the image name").DataType("string").Required(true)).
Param(ws.QueryParameter("secretName", "the secret name of the image repository").DataType("string")).
Filter(h.RbacService.CheckPerm("project/config", "list")).
Returns(200, "OK", v1.ImageInfo{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
ws.Filter(authCheckFilter)
return ws
}
func (h helmAPIInterface) listCharts(req *restful.Request, res *restful.Response) {
func (h repositoryAPIInterface) listCharts(req *restful.Request, res *restful.Response) {
url := utils.Sanitize(req.QueryParameter("repoUrl"))
secName := utils.Sanitize(req.QueryParameter("secretName"))
skipCache, err := isSkipCache(req)
@@ -98,7 +120,7 @@ func (h helmAPIInterface) listCharts(req *restful.Request, res *restful.Response
bcode.ReturnError(req, res, bcode.ErrSkipCacheParameter)
return
}
charts, err := h.HelmService.ListChartNames(context.Background(), url, secName, skipCache)
charts, err := h.HelmService.ListChartNames(req.Request.Context(), url, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -110,7 +132,7 @@ func (h helmAPIInterface) listCharts(req *restful.Request, res *restful.Response
}
}
func (h helmAPIInterface) listVersions(req *restful.Request, res *restful.Response) {
func (h repositoryAPIInterface) listVersions(req *restful.Request, res *restful.Response) {
url := req.QueryParameter("repoUrl")
chartName := req.PathParameter("chart")
secName := req.QueryParameter("secretName")
@@ -120,7 +142,7 @@ func (h helmAPIInterface) listVersions(req *restful.Request, res *restful.Respon
return
}
versions, err := h.HelmService.ListChartVersions(context.Background(), url, chartName, secName, skipCache)
versions, err := h.HelmService.ListChartVersions(req.Request.Context(), url, chartName, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -132,7 +154,7 @@ func (h helmAPIInterface) listVersions(req *restful.Request, res *restful.Respon
}
}
func (h helmAPIInterface) chartValues(req *restful.Request, res *restful.Response) {
func (h repositoryAPIInterface) chartValues(req *restful.Request, res *restful.Response) {
url := req.QueryParameter("repoUrl")
secName := req.QueryParameter("secretName")
chartName := req.PathParameter("chart")
@@ -143,7 +165,7 @@ func (h helmAPIInterface) chartValues(req *restful.Request, res *restful.Respons
return
}
versions, err := h.HelmService.GetChartValues(context.Background(), url, chartName, version, secName, skipCache)
versions, err := h.HelmService.GetChartValues(req.Request.Context(), url, chartName, version, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -155,9 +177,9 @@ func (h helmAPIInterface) chartValues(req *restful.Request, res *restful.Respons
}
}
func (h helmAPIInterface) listRepo(req *restful.Request, res *restful.Response) {
func (h repositoryAPIInterface) listRepo(req *restful.Request, res *restful.Response) {
project := req.QueryParameter("project")
repos, err := h.HelmService.ListChartRepo(context.Background(), project)
repos, err := h.HelmService.ListChartRepo(req.Request.Context(), project)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -169,6 +191,31 @@ func (h helmAPIInterface) listRepo(req *restful.Request, res *restful.Response)
}
}
func (h repositoryAPIInterface) getImageRepos(req *restful.Request, res *restful.Response) {
project := req.QueryParameter("project")
repos, err := h.ImageService.ListImageRepos(req.Request.Context(), project)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(v1.ListImageRegistryResponse{Registries: repos})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (h repositoryAPIInterface) getImageInfo(req *restful.Request, res *restful.Response) {
project := req.QueryParameter("project")
imageInfo := h.ImageService.GetImageInfo(req.Request.Context(), project, req.QueryParameter("secretName"), req.QueryParameter("name"))
err := res.WriteEntity(imageInfo)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func isSkipCache(req *restful.Request) (bool, error) {
skipStr := req.QueryParameter("skipCache")
skipCache := false

View File

@@ -264,6 +264,10 @@ func (h *AppHandler) collectHealthStatus(ctx context.Context, wl *appfile.Worklo
var traitStatusList []common.ApplicationTraitStatus
for _, tr := range wl.Traits {
if tr.FullTemplate.TraitDefinition.Spec.ControlPlaneOnly {
namespace = appRev.GetNamespace()
wl.Ctx.SetCtx(context.WithValue(wl.Ctx.GetCtx(), multicluster.ClusterContextKey, multicluster.ClusterLocalName))
}
var traitStatus = common.ApplicationTraitStatus{
Type: tr.Name,
Healthy: true,
@@ -277,6 +281,8 @@ func (h *AppHandler) collectHealthStatus(ctx context.Context, wl *appfile.Worklo
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appName, wl.Name, tr.Name)
}
traitStatusList = append(traitStatusList, traitStatus)
namespace = appRev.GetNamespace()
wl.Ctx.SetCtx(context.WithValue(wl.Ctx.GetCtx(), multicluster.ClusterContextKey, status.Cluster))
}
status.Traits = traitStatusList

View File

@@ -302,7 +302,7 @@ func strategyUnify(baseFile *ast.File, patchFile *ast.File, params *UnifyParams,
ret := baseInst.Value().Unify(patchInst.Value())
rv, err := toString(ret)
rv, err := toString(ret, removeTmpVar)
if err != nil {
return rv, errors.WithMessage(err, " format result toString")
}

View File

@@ -358,3 +358,38 @@ func listOpen(expr ast.Node) {
}
}
}
func removeTmpVar(expr ast.Node) ast.Node {
switch v := expr.(type) {
case *ast.File:
for _, decl := range v.Decls {
removeTmpVar(decl)
}
case *ast.Field:
removeTmpVar(v.Value)
case *ast.StructLit:
var elts []ast.Decl
for _, elt := range v.Elts {
if field, isField := elt.(*ast.Field); isField {
if ident, isIdent := field.Label.(*ast.Ident); isIdent && strings.HasPrefix(ident.Name, "_") {
continue
}
}
removeTmpVar(elt)
elts = append(elts, elt)
}
v.Elts = elts
case *ast.BinaryExpr:
removeTmpVar(v.X)
removeTmpVar(v.Y)
case *ast.EmbedDecl:
removeTmpVar(v.Expr)
case *ast.Comprehension:
removeTmpVar(v.Value)
case *ast.ListLit:
for _, elt := range v.Elts {
removeTmpVar(elt)
}
}
return expr
}

View File

@@ -23,6 +23,7 @@ import (
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/parser"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/require"
)
func TestWalk(t *testing.T) {
@@ -128,3 +129,33 @@ func TestWalk(t *testing.T) {
}
}
func TestRemoveTmpVar(t *testing.T) {
src := `spec: {
_tmp: "x"
list: [{
_tmp: "x"
retain: "y"
}, {
_tmp: "x"
retain: "z"
}]
retain: "y"
}
`
r := require.New(t)
var runtime cue.Runtime
inst, err := runtime.Compile("-", src)
r.NoError(err)
s, err := toString(inst.Value(), removeTmpVar)
r.NoError(err)
r.Equal(`spec: {
list: [{
retain: "y"
}, {
retain: "z"
}]
retain: "y"
}
`, s)
}

View File

@@ -302,9 +302,6 @@ func (ctx *templateContext) GetCtx() context.Context {
}
func (ctx *templateContext) SetCtx(newContext context.Context) {
if ctx.ctx != nil {
return
}
ctx.ctx = newContext
}

View File

@@ -22,6 +22,9 @@ import (
"fmt"
"github.com/briandowns/spinner"
prismclusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1"
clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
"github.com/oam-dev/cluster-register/pkg/hub"
"github.com/oam-dev/cluster-register/pkg/spoke"
"github.com/pkg/errors"
@@ -36,11 +39,7 @@ import (
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/policy/envbinding"
"github.com/oam-dev/kubevela/pkg/utils"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
@@ -49,6 +48,7 @@ import (
// KubeClusterConfig info for cluster management
type KubeClusterConfig struct {
FilePath string
ClusterName string
CreateNamespace string
*clientcmdapi.Config
@@ -84,17 +84,26 @@ func (clusterConfig *KubeClusterConfig) Validate() error {
return nil
}
// RegisterByVelaSecret create cluster secrets for KubeVela to use
func (clusterConfig *KubeClusterConfig) RegisterByVelaSecret(ctx context.Context, cli client.Client) error {
if err := ensureClusterNotExists(ctx, cli, clusterConfig.ClusterName); err != nil {
return errors.Wrapf(err, "cannot use cluster name %s", clusterConfig.ClusterName)
// PostRegistration try to create namespace after cluster registered. If failed, cluster will be unregistered.
func (clusterConfig *KubeClusterConfig) PostRegistration(ctx context.Context, cli client.Client) error {
if clusterConfig.CreateNamespace == "" {
return nil
}
if err := ensureNamespaceExists(ctx, cli, clusterConfig.ClusterName, clusterConfig.CreateNamespace); err != nil {
_ = DetachCluster(ctx, cli, clusterConfig.ClusterName, DetachClusterManagedClusterKubeConfigPathOption(clusterConfig.FilePath))
return fmt.Errorf("failed to ensure %s namespace installed in cluster %s: %w", clusterConfig.CreateNamespace, clusterConfig.ClusterName, err)
}
return nil
}
func (clusterConfig *KubeClusterConfig) createClusterSecret(ctx context.Context, cli client.Client, withEndpoint bool) error {
var credentialType clusterv1alpha1.CredentialType
data := map[string][]byte{
"endpoint": []byte(clusterConfig.Cluster.Server),
}
if !clusterConfig.Cluster.InsecureSkipTLSVerify {
data["ca.crt"] = clusterConfig.Cluster.CertificateAuthorityData
data := map[string][]byte{}
if withEndpoint {
data["endpoint"] = []byte(clusterConfig.Cluster.Server)
if !clusterConfig.Cluster.InsecureSkipTLSVerify {
data["ca.crt"] = clusterConfig.Cluster.CertificateAuthorityData
}
}
if len(clusterConfig.AuthInfo.Token) > 0 {
credentialType = clusterv1alpha1.CredentialTypeServiceAccountToken
@@ -115,22 +124,50 @@ func (clusterConfig *KubeClusterConfig) RegisterByVelaSecret(ctx context.Context
Type: corev1.SecretTypeOpaque,
Data: data,
}
if err := cli.Create(ctx, secret); err != nil {
return cli.Create(ctx, secret)
}
// RegisterByVelaSecret create cluster secrets for KubeVela to use
func (clusterConfig *KubeClusterConfig) RegisterByVelaSecret(ctx context.Context, cli client.Client) error {
if err := ensureClusterNotExists(ctx, cli, clusterConfig.ClusterName); err != nil {
return errors.Wrapf(err, "cannot use cluster name %s", clusterConfig.ClusterName)
}
if err := clusterConfig.createClusterSecret(ctx, cli, true); err != nil {
return errors.Wrapf(err, "failed to add cluster to kubernetes")
}
// TODO(somefive): create namespace now only work for cluster secret
if clusterConfig.CreateNamespace != "" {
if err := ensureNamespaceExists(ctx, cli, clusterConfig.ClusterName, clusterConfig.CreateNamespace); err != nil {
_ = cli.Delete(ctx, secret)
return errors.Wrapf(err, "failed to ensure %s namespace installed in cluster %s", clusterConfig.CreateNamespace, clusterConfig.ClusterName)
return clusterConfig.PostRegistration(ctx, cli)
}
// CreateBootstrapConfigMapIfNotExists alternative to
// https://github.com/kubernetes/kubernetes/blob/v1.24.1/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo/clusterinfo.go#L43
func CreateBootstrapConfigMapIfNotExists(ctx context.Context, cli client.Client) error {
cm := &corev1.ConfigMap{}
key := apitypes.NamespacedName{Namespace: metav1.NamespacePublic, Name: "cluster-info"}
if err := cli.Get(ctx, key, cm); err != nil {
if apierrors.IsNotFound(err) {
cm.ObjectMeta = metav1.ObjectMeta{Namespace: key.Namespace, Name: key.Name}
adminConfig, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
if err != nil {
return err
}
adminCluster := adminConfig.Contexts[adminConfig.CurrentContext].Cluster
bs, err := clientcmd.Write(clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{"": adminConfig.Clusters[adminCluster]},
})
if err != nil {
return err
}
cm.Data = map[string]string{"kubeconfig": string(bs)}
return cli.Create(ctx, cm)
}
return err
}
return nil
}
// RegisterClusterManagedByOCM create ocm managed cluster for use
// TODO(somefive): OCM ManagedCluster only support cli join now
func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.Context, args *JoinClusterArgs) error {
func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.Context, cli client.Client, args *JoinClusterArgs) error {
newTrackingSpinner := args.trackingSpinnerFactory
hubCluster, err := hub.NewHubCluster(args.hubConfig)
if err != nil {
@@ -164,6 +201,9 @@ func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.
return errors.Wrap(err, "fail to convert spoke-cluster kubeconfig")
}
if err = CreateBootstrapConfigMapIfNotExists(ctx, cli); err != nil {
return fmt.Errorf("failed to ensure cluster-info ConfigMap in kube-public namespace exists: %w", err)
}
spokeTracker := newTrackingSpinner("Building registration config for the managed cluster")
spokeTracker.FinalMSG = "Successfully prepared registration config.\n"
spokeTracker.Start()
@@ -172,6 +212,7 @@ func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.
args.ioStreams.Infof("Using the api endpoint from hub kubeconfig %q as registration entry.\n", args.hubConfig.Host)
overridingRegistrationEndpoint = args.hubConfig.Host
}
hubKubeToken, err := hubCluster.GenerateHubClusterKubeConfig(ctx, overridingRegistrationEndpoint)
if err != nil {
return errors.Wrap(err, "fail to generate the token for spoke-cluster")
@@ -230,7 +271,7 @@ func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.
// LoadKubeClusterConfigFromFile create KubeClusterConfig from kubeconfig file
func LoadKubeClusterConfigFromFile(filepath string) (*KubeClusterConfig, error) {
clusterConfig := &KubeClusterConfig{}
clusterConfig := &KubeClusterConfig{FilePath: filepath}
var err error
clusterConfig.Config, err = clientcmd.LoadFromFile(filepath)
if err != nil {
@@ -345,7 +386,7 @@ func JoinClusterByKubeConfig(ctx context.Context, cli client.Client, kubeconfigP
return nil, errors.Wrapf(err, "failed to determine the registration endpoint for the hub cluster "+
"when parsing --in-cluster-bootstrap flag")
}
if err = clusterConfig.RegisterClusterManagedByOCM(ctx, args); err != nil {
if err = clusterConfig.RegisterClusterManagedByOCM(ctx, cli, args); err != nil {
return clusterConfig, err
}
}
@@ -384,12 +425,12 @@ func DetachCluster(ctx context.Context, cli client.Client, clusterName string, o
if clusterName == ClusterLocalName {
return ErrReservedLocalClusterName
}
vc, err := GetVirtualCluster(ctx, cli, clusterName)
vc, err := prismclusterv1alpha1.NewClusterClient(cli).Get(ctx, clusterName)
if err != nil {
return err
}
switch vc.Type {
switch vc.Spec.CredentialType {
case clusterv1alpha1.CredentialTypeX509Certificate, clusterv1alpha1.CredentialTypeServiceAccountToken:
clusterSecret, err := getMutableClusterSecret(ctx, cli, clusterName)
if err != nil {
@@ -398,7 +439,7 @@ func DetachCluster(ctx context.Context, cli client.Client, clusterName string, o
if err := cli.Delete(ctx, clusterSecret); err != nil {
return errors.Wrapf(err, "failed to detach cluster %s", clusterName)
}
case types.CredentialTypeOCMManagedCluster:
case prismclusterv1alpha1.CredentialTypeOCMManagedCluster:
if args.managedClusterKubeConfigPath == "" {
return errors.New("kubeconfig-path must be set to detach ocm managed cluster")
}
@@ -416,7 +457,7 @@ func DetachCluster(ctx context.Context, cli client.Client, clusterName string, o
return err
}
managedCluster := ocmclusterv1.ManagedCluster{ObjectMeta: metav1.ObjectMeta{Name: clusterName}}
if err = cli.Delete(context.Background(), &managedCluster); err != nil {
if err = cli.Delete(ctx, &managedCluster); err != nil {
if !apierrors.IsNotFound(err) {
return err
}

View File

@@ -49,6 +49,7 @@ func newDeleteConfig(options ...DeleteOption) *deleteConfig {
// Delete delete resources
func (h *resourceKeeper) Delete(ctx context.Context, manifests []*unstructured.Unstructured, options ...DeleteOption) (err error) {
h.ClearNamespaceForClusterScopedResources(manifests)
if err = h.AdmissionCheck(ctx, manifests); err != nil {
return err
}

View File

@@ -59,6 +59,7 @@ func (h *resourceKeeper) Dispatch(ctx context.Context, manifests []*unstructured
if h.applyOncePolicy != nil && h.applyOncePolicy.Enable && h.applyOncePolicy.Rules == nil {
options = append(options, MetaOnlyOption{})
}
h.ClearNamespaceForClusterScopedResources(manifests)
// 0. check admission
if err = h.AdmissionCheck(ctx, manifests); err != nil {
return err

View File

@@ -0,0 +1,35 @@
/*
Copyright 2022 The KubeVela 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 resourcekeeper
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// ClearNamespaceForClusterScopedResources clear namespace for cluster scoped resources
func (h *resourceKeeper) ClearNamespaceForClusterScopedResources(manifests []*unstructured.Unstructured) {
for _, manifest := range manifests {
mappings, err := h.Client.RESTMapper().RESTMappings(manifest.GroupVersionKind().GroupKind(), manifest.GroupVersionKind().Version)
if err != nil {
continue
}
if len(mappings) > 0 && mappings[0].Scope.Name() == meta.RESTScopeNameRoot {
manifest.SetNamespace("")
}
}
}

View File

@@ -0,0 +1,55 @@
/*
Copyright 2022 The KubeVela 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 resourcekeeper
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
var _ = Describe("Test ResourceKeeper utilities", func() {
It("Test ClearNamespaceForClusterScopedResources", func() {
cli := testClient
h := &resourceKeeper{
Client: cli,
app: &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "default"}},
applicator: apply.NewAPIApplicator(cli),
cache: newResourceCache(cli),
}
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "vela"}}
nsObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ns)
Expect(err).Should(Succeed())
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "vela"}}
cmObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cm)
Expect(err).Should(Succeed())
uns := []*unstructured.Unstructured{{Object: nsObj}, {Object: cmObj}}
uns[0].SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace"))
uns[1].SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
h.ClearNamespaceForClusterScopedResources(uns)
Expect(uns[0].GetNamespace()).Should(Equal(""))
Expect(uns[1].GetNamespace()).Should(Equal("vela"))
})
})

View File

@@ -1,89 +0,0 @@
/*
Copyright 2022 The KubeVela 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 image
import (
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// Meta is the struct for image metadata
type Meta struct {
Registry string
Repository string
Name string
Tag string
}
// DockerHubImageTagResponse is the struct for docker hub image tag response
type DockerHubImageTagResponse struct {
Count int `json:"count"`
Results []Result
}
// Result is the struct for docker hub image tag result
type Result struct {
Name string `json:"name"`
}
// IsExisted checks whether a public or private image exists
func IsExisted(username, password, image string) (bool, error) {
ref, err := name.ParseReference(image)
if err != nil {
return false, err
}
var img v1.Image
if username != "" || password != "" {
basic := &authn.Basic{
Username: username,
Password: password,
}
option := remote.WithAuth(basic)
img, err = remote.Image(ref, option)
if err != nil {
return false, err
}
} else {
img, err = remote.Image(ref)
if err != nil {
return false, err
}
}
_, err = img.Digest()
if err != nil {
return false, err
}
_, err = img.Manifest()
if err != nil {
return false, err
}
return true, nil
}
// RegistryMeta is the struct for registry metadata
type RegistryMeta struct {
Username string `json:"username"`
Password string `json:"password"`
}

View File

@@ -1,135 +0,0 @@
/*
Copyright 2022 The KubeVela 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 image
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExisted(t *testing.T) {
type args struct {
username string
password string
image string
}
type want struct {
existed bool
errMsg string
}
testcases := map[string]struct {
args args
want want
}{
"empty image name": {
args: args{
image: "",
},
want: want{
existed: false,
errMsg: "could not parse referenc",
},
},
"just image name": {
args: args{
image: "nginx",
},
want: want{
existed: true,
},
},
" image name with registry": {
args: args{
image: "docker.io/nginx",
},
want: want{
existed: true,
},
},
"image name with tag": {
args: args{
image: "nginx:latest",
},
want: want{
existed: true,
},
},
"image name with repository": {
args: args{
image: "library/nginx:latest",
},
want: want{
existed: true,
},
},
"image name with registry and repository": {
args: args{
image: "docker.io/library/nginx",
},
want: want{
existed: true,
},
},
"invalid image name": {
args: args{
image: "jfsdfjwfjwf:fwefsfsjflwejfwjfoewfsffsfw",
},
want: want{
existed: false,
errMsg: "UNAUTHORIZED",
},
},
"invalid image registry": {
args: args{
image: "nginx1sf/jfsdfjwfjwf:fwefsfsjflwejfwjfoewfsffsfw",
},
want: want{
existed: false,
errMsg: "UNAUTHORIZED",
},
},
"not docker hub registry": {
args: args{
image: "alibabacloud.com/d/e:v0.1",
},
want: want{
existed: false,
errMsg: "invalid character '<' looking for beginning of value",
},
},
"private registry, invalid authentication": {
args: args{
image: "abcfefsfjflsfjweffwe73rr.com/d/e:v0.1",
username: "admin",
},
want: want{
existed: false,
errMsg: "Get",
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
got, err := IsExisted(tc.args.username, tc.args.password, tc.args.image)
if err != nil || tc.want.errMsg != "" {
assert.Contains(t, err.Error(), tc.want.errMsg)
}
assert.Equal(t, got, tc.want.existed)
})
}
}

View File

@@ -38,9 +38,12 @@ const (
addonGitToken = "gitToken"
addonOssType = "OSS"
addonGitType = "git"
addonGiteeType = "gitee"
addonGitlabType = "gitlab"
addonHelmType = "helm"
addonUsername = "username"
addonPassword = "password"
addonRepoName = "repoName"
)
// NewAddonRegistryCommand return an addon registry command
@@ -344,6 +347,37 @@ func getRegistryFromArgs(cmd *cobra.Command, args []string) (*pkgaddon.Registry,
return nil, err
}
r.Git.Token = token
case addonGiteeType:
r.Gitee = &pkgaddon.GiteeAddonSource{}
r.Gitee.URL = endpoint
path, err := cmd.Flags().GetString(addonPath)
if err != nil {
return nil, err
}
r.Gitee.Path = path
token, err := cmd.Flags().GetString(addonGitToken)
if err != nil {
return nil, err
}
r.Gitee.Token = token
case addonGitlabType:
r.Gitlab = &pkgaddon.GitlabAddonSource{}
r.Gitlab.URL = endpoint
path, err := cmd.Flags().GetString(addonPath)
if err != nil {
return nil, err
}
r.Gitlab.Path = path
token, err := cmd.Flags().GetString(addonGitToken)
if err != nil {
return nil, err
}
r.Gitlab.Token = token
gitLabRepoName, err := cmd.Flags().GetString(addonRepoName)
if err != nil {
return nil, err
}
r.Gitlab.Repo = gitLabRepoName
case addonHelmType:
r.Helm = &pkgaddon.HelmSource{}
r.Helm.URL = endpoint

View File

@@ -24,6 +24,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/fatih/color"
prismclusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1"
"github.com/oam-dev/cluster-gateway/pkg/config"
"github.com/oam-dev/cluster-gateway/pkg/generated/clientset/versioned"
"github.com/pkg/errors"
@@ -107,11 +108,11 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
if err != nil {
return err
}
clusters, err := multicluster.ListVirtualClusters(context.Background(), client)
clusters, err := prismclusterv1alpha1.NewClusterClient(client).List(context.Background())
if err != nil {
return errors.Wrap(err, "fail to get registered cluster")
}
for _, cluster := range clusters {
for _, cluster := range clusters.Items {
var labels []string
for k, v := range cluster.Labels {
if !strings.HasPrefix(k, config.MetaApiGroupName) {
@@ -124,7 +125,7 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
}
for i, l := range labels {
if i == 0 {
table.AddRow(cluster.Name, cluster.Alias, cluster.Type, cluster.EndPoint, fmt.Sprintf("%v", cluster.Accepted), l)
table.AddRow(cluster.Name, cluster.Spec.Alias, cluster.Spec.CredentialType, cluster.Spec.Endpoint, fmt.Sprintf("%v", cluster.Spec.Accepted), l)
} else {
table.AddRow("", "", "", "", "", l)
}

View File

@@ -1,6 +1,7 @@
import (
"encoding/base64"
"encoding/json"
"strconv"
)
"config-image-registry": {
@@ -39,8 +40,8 @@ template: {
if parameter.auth == _|_ {
type: "Opaque"
}
if parameter.auth != _|_ {
stringData: {
stringData: {
if parameter.auth != _|_ && parameter.auth.username != _|_ {
".dockerconfigjson": json.Marshal({
"auths": "\(parameter.registry)": {
"username": parameter.auth.username
@@ -52,11 +53,17 @@ template: {
}
})
}
if parameter.insecure != _|_ {
"insecure-skip-verify": strconv.FormatBool(parameter.insecure)
}
if parameter.useHTTP != _|_ {
"protocol-use-http": strconv.FormatBool(parameter.useHTTP)
}
}
}
parameter: {
// +usage=Image registry FQDN
// +usage=Image registry FQDN, such as: index.docker.io
registry: string
// +usage=Authenticate the image registry
auth?: {
@@ -67,5 +74,9 @@ template: {
// +usage=Private Image registry email
email?: string
}
// +usage=For the registry server that uses the self-signed certificate
insecure?: bool
// +usage=For the registry server that uses the HTTP protocol
useHTTP?: bool
}
}