feat: add artifact command.

Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
joyceliu 2024-06-14 16:35:29 +08:00
parent f48624e097
commit fe588fad3a
24 changed files with 654 additions and 105 deletions

View File

@ -0,0 +1,12 @@
- hosts:
- localhost
roles:
- init/init-artifact
tasks:
- name: Package image
image:
pull: "{{ image_manifests }}"
when: image_manifests|length > 0
- name: Export artifact
command: |
cd {{ work_dir }} && tar -czvf kubekey-artifact.tar.gz kubekey/

View File

@ -0,0 +1,6 @@
- hosts:
- localhost
tags: ["always"]
roles:
- init/init-artifact
- install/image-registry

View File

@ -3,7 +3,7 @@
- localhost
roles:
- role: precheck/artifact_check
when: artifact.artifact_file | defined
when: artifact_file | defined
- hosts:
- k8s_cluster

View File

@ -88,11 +88,6 @@ artifact:
# {% if (kkzone == 'cn') %}https://kubernetes-release.pek3b.qingstor.com/osixia/keepalived/releases/download/{{ keepalived_version }}/keepalived-{{ keepalived_version }}-linux-amd64.tgz{% else %}https://github.com/osixia/keepalived/releases/download/{{ keepalived_version }}/keepalived-{{ keepalived_version }}-linux-amd64.tgz{% endif %}
# arm64: |
# {% if (kkzone == 'cn') %}https://kubernetes-release.pek3b.qingstor.com/osixia/keepalived/releases/download/{{ keepalived_version }}/keepalived-{{ keepalived_version }}-linux-arm64.tgz{% else %}https://github.com/osixia/keepalived/releases/download/{{ keepalived_version }}/keepalived-{{ keepalived_version }}-linux-arm64.tgz{% endif %}
oras:
amd64: |
https://github.com/oras-project/oras/releases/download/{{ oras_version }}/oras_{{ oras_version|slice:'1:' }}_linux_amd64.tar.gz
arm64: |
https://github.com/oras-project/oras/releases/download/{{ oras_version }}/oras_{{ oras_version|slice:'1:' }}_linux_arm64.tar.gz
cilium: https://helm.cilium.io/cilium-{{ cilium_version }}.tgz
kubeovn: https://kubeovn.github.io/kube-ovn/kube-ovn-{{ kubeovn_version }}.tgz
hybridnet: https://github.com/alibaba/hybridnet/releases/download/helm-chart-{{ hybridnet_version }}/hybridnet-{{ hybridnet_version }}.tgz

View File

@ -264,21 +264,3 @@
loop: "{{ artifact.arch }}"
when:
- keepalived_version | defined && keepalived_version != ""
- name: Check binaries for oras
command: |
artifact_name={{ artifact.artifact_url.oras[item]|split:"/"|last }}
artifact_path={{ work_dir }}/kubekey/oras/{{ oras_version }}/{{ item }}
if [ ! -f $artifact_path/$artifact_name ]; then
mkdir -p $artifact_path
# download online
http_code=$(curl -Lo /dev/null -s -w "%{http_code}" {{ artifact.artifact_url.oras[item] }})
if [ $http_code != 200 ]; then
echo "http code is $http_code"
exit 1
fi
curl -L -o $artifact_path/$artifact_name {{ artifact.artifact_url.oras[item] }}
fi
loop: "{{ artifact.arch }}"
when:
- oras_version | defined && oras_version != ""

View File

@ -1,16 +1,18 @@
---
- name: Create work_dir
tags: ["always"]
command: |
if [ ! -d "{{ work_dir }}" ]; then
mkdir -p {{ work_dir }}
fi
- name: Extract artifact to work_dir
tags: ["always"]
command: |
if [ ! -f "{{ artifact.artifact_file }}" ]; then
tar -zxvf {{ artifact.artifact_file }} -C {{ work_dir }}
if [ ! -f "{{ artifact_file }}" ]; then
tar -zxvf {{ artifact_file }} -C {{ work_dir }}
fi
when: artifact.artifact_file | defined
when: artifact_file | defined
- name: Download binaries
block:
@ -24,5 +26,6 @@
- include_tasks: pki.yaml
- name: Chown work_dir to sudo
tags: ["always"]
command: |
chown -R ${SUDO_UID}:${SUDO_GID} {{ work_dir }}

View File

@ -1,10 +1,13 @@
image_registry:
# ha_vip: 192.168.122.59
namespace_override: ""
auth:
registry: "{% if (image_registry.ha_vip|defined) %}{{ image_registry.ha_vip }}{% else %}{{ groups['image_registry']|first }}{% endif %}"
username: admin
password: Harbor12345
# registry type. support: harbor, registry
type: harbor
# Virtual IP address for repository High Availability. the Virtual IP address should be available.
# ha_vip: 192.168.122.59
harbor:
admin_password: Harbor12345
registry:
version: 2
config:

View File

@ -1,27 +1,6 @@
---
- name: Check if image to load
ignore_errors: true
command: |
ls {{ work_dir }}/kubekey/images/
register: local_images_dir
- name: Sync oras to remote
copy:
src: "{{ work_dir }}/kubekey/oras/{{ oras_version }}/{{ binary_type.stdout }}/oras_{{ oras_version|slice:'1:' }}_linux_{{ binary_type.stdout }}.tar.gz"
dest: "/tmp/kubekey/oras_{{ oras_version|slice:'1:' }}_linux_{{ binary_type.stdout }}.tar.gz"
when: local_images_dir.stderr == ""
- name: Unpackage oras binary
command: tar -zxvf /tmp/kubekey/oras_{{ oras_version|slice:'1:' }}_linux_{{ binary_type.stdout }}.tar.gz -C /usr/local/bin oras
when: local_images_dir.stderr == ""
- name: Sync images package to remote
copy:
src: "{{ work_dir }}/kubekey/images/"
dest: "/tmp/kubekey/images/"
when: local_images_dir.stderr == ""
- name: Sync images to registry
- name: Create harbor project for each image
tags: ["only_image"]
command: |
for dir in /tmp/kubekey/images/*; do
if [ ! -d "$dir" ]; then
@ -40,12 +19,34 @@
fi
# if project is not exist, create if
http_code=$(curl -Iks -u "admin:{{ image_registry.harbor.admin_password }}" 'https://localhost/api/v2.0/projects?project_name=${project}' | grep HTTP | awk '{print $2}')
http_code=$(curl -Iks -u "{{ image_registry.auth.username }}:{{ image_registry.auth.password }}" 'https://localhost/api/v2.0/projects?project_name=${project}' | grep HTTP | awk '{print $2}')
if [ $http_code == 404 ]; then
# create project
curl -u "admin:{{ image_registry.harbor.admin_password }}" -k -X POST -H "Content-Type: application/json" "https://localhost/api/v2.0/projects" -d "{ \"project_name\": \"${project}\", \"public\": true}"
fi
oras cp --to-username admin --to-password {{ image_registry.harbor.admin_password }} ${dir##*/} localhost/${project}/${dest_image}:${tag}
curl -u "{{ image_registry.auth.username }}:{{ image_registry.auth.password }}" -k -X POST -H "Content-Type: application/json" "https://localhost/api/v2.0/projects" -d "{ \"project_name\": \"${project}\", \"public\": true}"
fi
done
when: local_images_dir.stderr == ""
when:
- image_registry.type == 'harbor'
- image_registry.namespace_override == ""
-
- name: Create harbor project for namespace_override
tags: ["only_image"]
command: |
# if project is not exist, create if
http_code=$(curl -Iks -u "{{ image_registry.auth.username }}:{{ image_registry.auth.password }}" 'https://localhost/api/v2.0/projects?project_name={{ image_registry.namespace_override }}' | grep HTTP | awk '{print $2}')
if [ $http_code == 404 ]; then
# create project
curl -u "{{ image_registry.auth.username }}:{{ image_registry.auth.password }}" -k -X POST -H "Content-Type: application/json" "https://localhost/api/v2.0/projects" -d "{ \"project_name\": \"{{ image_registry.namespace_override }}\", \"public\": true}"
fi
when:
- image_registry.type == 'harbor'
- image_registry.namespace_override != ""
- name: Sync images package to harbor
tags: ["only_image"]
image:
push:
registry: "{{ image_registry.auth.registry }}"
namespace_override: "{{ image_registry.namespace_override }}"
username: "{{ image_registry.auth.username }}"
password: "{{ image_registry.auth.password }}"

View File

@ -14,3 +14,4 @@
when: image_registry.type == 'harbor'
- include_tasks: load_images.yaml
tags: ["only_image"]

View File

@ -34,7 +34,7 @@ https:
# The initial password of Harbor admin
# It only works in first time to install harbor
# Remember Change the admin password from UI after launching Harbor.
harbor_admin_password: {{ image_registry.harbor.admin_password }}
harbor_admin_password: {{ image_registry.auth.password }}
# Harbor DB configuration
database:

View File

@ -1,20 +1,20 @@
---
- name: Check artifact is exits
command:
if [ ! -f "{{ artifact.artifact_file }}" ]; then
if [ ! -f "{{ artifact_file }}" ]; then
exit 1
fi
- name: Check artifact file type
command:
if [[ "{{ artifact.artifact_file }}" != *{{ item }} ]]; then
if [[ "{{ artifact_file }}" != *{{ item }} ]]; then
exit 1
fi
loop: ['.tgz','.tar.gz']
- name: Check md5 of artifact
command:
if [[ $(md5sum {{ artifact.artifact_file }}) != {{ artifact.artifact_md5 }} ]]; then
if [[ $(md5sum {{ artifact_file }}) != {{ artifact.artifact_md5 }} ]]; then
exit 1
fi
when:

101
cmd/kk/app/artifact.go Normal file
View File

@ -0,0 +1,101 @@
/*
Copyright 2024 The KubeSphere 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 app
import (
"io/fs"
"os"
"github.com/spf13/cobra"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"github.com/kubesphere/kubekey/v4/cmd/kk/app/options"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
)
func newArtifactCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "artifact",
Short: "Manage a KubeKey offline installation package",
}
cmd.AddCommand(newArtifactExportCommand())
cmd.AddCommand(newArtifactImagesCommand())
return cmd
}
func newArtifactExportCommand() *cobra.Command {
o := options.NewArtifactExportOptions()
cmd := &cobra.Command{
Use: "export",
Short: "Export a KubeKey offline installation package",
RunE: func(cmd *cobra.Command, args []string) error {
pipeline, config, inventory, err := o.Complete(cmd, []string{"playbooks/artifact_export.yaml"})
if err != nil {
return err
}
// set workdir
_const.SetWorkDir(o.WorkDir)
// create workdir directory,if not exists
if _, err := os.Stat(o.WorkDir); os.IsNotExist(err) {
if err := os.MkdirAll(o.WorkDir, fs.ModePerm); err != nil {
return err
}
}
return run(signals.SetupSignalHandler(), pipeline, config, inventory)
},
}
flags := cmd.Flags()
for _, f := range o.Flags().FlagSets {
flags.AddFlagSet(f)
}
return cmd
}
func newArtifactImagesCommand() *cobra.Command {
o := options.NewArtifactImagesOptions()
cmd := &cobra.Command{
Use: "images",
Short: "push images to a registry from an artifact",
RunE: func(cmd *cobra.Command, args []string) error {
pipeline, config, inventory, err := o.Complete(cmd, []string{"playbooks/artifact_images.yaml"})
if err != nil {
return err
}
// set workdir
_const.SetWorkDir(o.WorkDir)
// create workdir directory,if not exists
if _, err := os.Stat(o.WorkDir); os.IsNotExist(err) {
if err := os.MkdirAll(o.WorkDir, fs.ModePerm); err != nil {
return err
}
}
return run(signals.SetupSignalHandler(), pipeline, config, inventory)
},
}
flags := cmd.Flags()
for _, f := range o.Flags().FlagSets {
flags.AddFlagSet(f)
}
return cmd
}
func init() {
registerInternalCommand(newArtifactCommand())
}

View File

@ -0,0 +1,124 @@
/*
Copyright 2024 The KubeSphere 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 options
import (
"fmt"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cliflag "k8s.io/component-base/cli/flag"
kubekeyv1 "github.com/kubesphere/kubekey/v4/pkg/apis/kubekey/v1"
)
// ======================================================================================
// artifact export
// ======================================================================================
type ArtifactExportOptions struct {
CommonOptions
}
func (o *ArtifactExportOptions) Flags() cliflag.NamedFlagSets {
fss := o.CommonOptions.Flags()
return fss
}
func (o ArtifactExportOptions) Complete(cmd *cobra.Command, args []string) (*kubekeyv1.Pipeline, *kubekeyv1.Config, *kubekeyv1.Inventory, error) {
pipeline := &kubekeyv1.Pipeline{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "artifact-export-",
Namespace: o.Namespace,
Annotations: map[string]string{
kubekeyv1.BuiltinsProjectAnnotation: "",
},
},
}
// complete playbook. now only support one playbook
if len(args) == 1 {
o.Playbook = args[0]
} else {
return nil, nil, nil, fmt.Errorf("%s\nSee '%s -h' for help and examples", cmd.Use, cmd.CommandPath())
}
pipeline.Spec = kubekeyv1.PipelineSpec{
Playbook: o.Playbook,
Debug: o.Debug,
}
config, inventory, err := o.completeRef(pipeline)
if err != nil {
return nil, nil, nil, err
}
return pipeline, config, inventory, nil
}
func NewArtifactExportOptions() *ArtifactExportOptions {
// set default value
return &ArtifactExportOptions{CommonOptions: newCommonOptions()}
}
// ======================================================================================
// artifact image
// ======================================================================================
type ArtifactImagesOptions struct {
CommonOptions
}
func (o *ArtifactImagesOptions) Flags() cliflag.NamedFlagSets {
fss := o.CommonOptions.Flags()
return fss
}
func (o ArtifactImagesOptions) Complete(cmd *cobra.Command, args []string) (*kubekeyv1.Pipeline, *kubekeyv1.Config, *kubekeyv1.Inventory, error) {
pipeline := &kubekeyv1.Pipeline{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "artifact-images-",
Namespace: o.Namespace,
Annotations: map[string]string{
kubekeyv1.BuiltinsProjectAnnotation: "",
},
},
}
// complete playbook. now only support one playbook
if len(args) == 1 {
o.Playbook = args[0]
} else {
return nil, nil, nil, fmt.Errorf("%s\nSee '%s -h' for help and examples", cmd.Use, cmd.CommandPath())
}
pipeline.Spec = kubekeyv1.PipelineSpec{
Playbook: o.Playbook,
Debug: o.Debug,
Tags: []string{"only_image"},
}
config, inventory, err := o.completeRef(pipeline)
if err != nil {
return nil, nil, nil, err
}
return pipeline, config, inventory, nil
}
func NewArtifactImagesOptions() *ArtifactImagesOptions {
// set default value
return &ArtifactImagesOptions{CommonOptions: newCommonOptions()}
}

View File

@ -37,8 +37,6 @@ type CreateClusterOptions struct {
Kubernetes string
// ContainerRuntime for kubernetes. Such as docker, containerd etc.
ContainerManager string
// Artifact container all binaries which used to install kubernetes.
Artifact string
}
func (o *CreateClusterOptions) Flags() cliflag.NamedFlagSets {
@ -46,7 +44,6 @@ func (o *CreateClusterOptions) Flags() cliflag.NamedFlagSets {
kfs := fss.FlagSet("config")
kfs.StringVar(&o.Kubernetes, "with-kubernetes", "", "Specify a supported version of kubernetes")
kfs.StringVar(&o.ContainerManager, "container-manager", "", "Container runtime: docker, crio, containerd and isula.")
kfs.StringVarP(&o.Artifact, "artifact", "a", "", "Path to a KubeKey artifact")
return fss
}
@ -88,12 +85,6 @@ func (o *CreateClusterOptions) Complete(cmd *cobra.Command, args []string) (*kub
return nil, nil, nil, err
}
}
if o.Artifact != "" {
// override artifact_file in config
if err := config.SetValue("artifact_file", o.Artifact); err != nil {
return nil, nil, nil, err
}
}
return pipeline, config, inventory, nil
}

View File

@ -32,14 +32,10 @@ import (
type InitOSOptions struct {
CommonOptions
// Artifact container all binaries which used to install kubernetes.
Artifact string
}
func (o *InitOSOptions) Flags() cliflag.NamedFlagSets {
fss := o.CommonOptions.Flags()
kfs := fss.FlagSet("config")
kfs.StringVarP(&o.Artifact, "artifact", "a", "", "Path to a KubeKey artifact")
return fss
}
@ -69,12 +65,6 @@ func (o InitOSOptions) Complete(cmd *cobra.Command, args []string) (*kubekeyv1.P
if err != nil {
return nil, nil, nil, err
}
if o.Artifact != "" {
// override artifact_file in config
if err := config.SetValue("artifact_file", o.Artifact); err != nil {
return nil, nil, nil, err
}
}
return pipeline, config, inventory, nil
}
@ -85,19 +75,15 @@ func NewInitOSOptions() *InitOSOptions {
}
// ======================================================================================
// init registry
// init registry
// ======================================================================================
type InitRegistryOptions struct {
CommonOptions
// Artifact container all binaries which used to install kubernetes.
Artifact string
}
func (o *InitRegistryOptions) Flags() cliflag.NamedFlagSets {
fss := o.CommonOptions.Flags()
kfs := fss.FlagSet("config")
kfs.StringVarP(&o.Artifact, "artifact", "a", "", "Path to a KubeKey artifact")
return fss
}
@ -127,12 +113,6 @@ func (o InitRegistryOptions) Complete(cmd *cobra.Command, args []string) (*kubek
if err != nil {
return nil, nil, nil, err
}
if o.Artifact != "" {
// override artifact_file in config
if err := config.SetValue("artifact_file", o.Artifact); err != nil {
return nil, nil, nil, err
}
}
return pipeline, config, inventory, nil
}

View File

@ -56,6 +56,8 @@ type CommonOptions struct {
Set []string
// WorkDir is the baseDir which command find any resource (project etc.)
WorkDir string
// Artifact is the path of offline package for kubekey.
Artifact string
// Debug mode, after a successful execution of Pipeline, will retain runtime data, which includes task execution status and parameters.
Debug bool
// Namespace for all resources.
@ -80,6 +82,7 @@ func (o *CommonOptions) Flags() cliflag.NamedFlagSets {
fss := cliflag.NamedFlagSets{}
gfs := fss.FlagSet("generic")
gfs.StringVar(&o.WorkDir, "work-dir", o.WorkDir, "the base Dir for kubekey. Default current dir. ")
gfs.StringVarP(&o.Artifact, "artifact", "a", "", "Path to a KubeKey artifact")
gfs.StringVarP(&o.ConfigFile, "config", "c", o.ConfigFile, "the config file path. support *.yaml ")
gfs.StringArrayVar(&o.Set, "set", o.Set, "set value in config. format --set key=val")
gfs.StringVarP(&o.InventoryFile, "inventory", "i", o.InventoryFile, "the host list file path. support *.ini")
@ -108,6 +111,13 @@ func (o *CommonOptions) completeRef(pipeline *kubekeyv1.Pipeline) (*kubekeyv1.Co
} else if err := config.SetValue("work_dir", o.WorkDir); err != nil {
return nil, nil, fmt.Errorf("work_dir to config error: %w", err)
}
if o.Artifact != "" {
// override artifact_file in config
if err := config.SetValue("artifact_file", o.Artifact); err != nil {
return nil, nil, fmt.Errorf("artifact file to config error: %w", err)
}
}
for _, s := range o.Set {
ss := strings.Split(s, "=")
if len(ss) != 2 {

5
go.mod
View File

@ -22,6 +22,7 @@ require (
k8s.io/component-base v0.29.1
k8s.io/klog/v2 v2.120.1
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
oras.land/oras-go/v2 v2.5.0
sigs.k8s.io/controller-runtime v0.17.0
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
sigs.k8s.io/yaml v1.4.0
@ -75,6 +76,8 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
@ -104,7 +107,7 @@ require (
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.14.0 // indirect

10
go.sum
View File

@ -167,6 +167,10 @@ github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -302,8 +306,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -407,6 +411,8 @@ k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 h1:m6dl1pkxz3HuE2mP9MUYPC
k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4=
sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s=

View File

@ -49,12 +49,10 @@ func TestParseBool(t *testing.T) {
},
{
name: "container string",
condition: []string{"instr[0].test"},
condition: []string{"test in instr"},
variable: pongo2.Context{
"instr": []pongo2.Context{
{"test": true},
{"test": false},
},
"test": "a1",
"instr": "vda hjilsa1 sdte",
},
excepted: true,
},

View File

@ -261,7 +261,7 @@ func appendSANsToAltNames(altNames *certutil.AltNames, SANs []string, certName s
} else if len(validation.IsWildcardDNS1123Subdomain(altname)) == 0 {
altNames.DNSNames = append(altNames.DNSNames, altname)
} else {
klog.Warningf(
klog.V(4).Infof(
"[certificates] WARNING: '%s' was not added to the '%s' SAN, because it is not a valid IP or RFC-1123 compliant DNS entry\n",
altname,
certName,

View File

@ -1,3 +1,19 @@
/*
Copyright 2024 The KubeSphere 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 modules
import (

298
pkg/modules/image.go Normal file
View File

@ -0,0 +1,298 @@
/*
Copyright 2024 The KubeSphere 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 modules
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
imagev1 "github.com/opencontainers/image-spec/specs-go/v1"
"k8s.io/klog/v2"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/retry"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
func ModuleImage(ctx context.Context, options ExecOptions) (stdout string, stderr string) {
// get host variable
ha, err := options.Variable.Get(variable.GetAllVariable(options.Host))
if err != nil {
klog.V(4).ErrorS(err, "failed to get host variable", "hostname", options.Host)
return "", err.Error()
}
// check args
args := variable.Extension2Variables(options.Args)
pullParam, _ := variable.StringSliceVar(ha.(map[string]any), args, "pull")
// if namespace_override is not empty, it will override the image manifests namespace_override. (namespace maybe multi sub path)
// push to private registry
pushParam := args["push"]
// pull image manifests to local dir
for _, img := range pullParam {
src, err := remote.NewRepository(img)
if err != nil {
return "", fmt.Sprintf("failed to get remote image: %v", err)
}
dst, err := NewLocalRepository(filepath.Join(domain, src.Reference.Repository) + ":" + src.Reference.Reference)
if err != nil {
return "", fmt.Sprintf("failed to get local image: %v", err)
}
if _, err = oras.Copy(context.Background(), src, src.Reference.Reference, dst, "", oras.DefaultCopyOptions); err != nil {
return "", fmt.Sprintf("failed to copy image: %v", err)
}
}
// push image to private registry
if pushParam != nil {
registry, _ := variable.StringVar(ha.(map[string]any), pushParam.(map[string]any), "registry")
username, _ := variable.StringVar(ha.(map[string]any), pushParam.(map[string]any), "username")
password, _ := variable.StringVar(ha.(map[string]any), pushParam.(map[string]any), "password")
namespace, _ := variable.StringVar(ha.(map[string]any), pushParam.(map[string]any), "namespace_override")
manifests, err := findLocalImageManifests(filepath.Join(_const.GetWorkDir(), "kubekey", "images"))
if err != nil {
return "", fmt.Sprintf("failed to find local image manifests: %v", err)
}
for _, img := range manifests {
src, err := NewLocalRepository(filepath.Join(domain, img))
if err != nil {
return "", fmt.Sprintf("failed to get local image: %v", err)
}
repo := src.Reference.Repository
if namespace != "" {
repo = filepath.Join(namespace, filepath.Base(repo))
}
dst, err := remote.NewRepository(filepath.Join(registry, repo) + ":" + src.Reference.Reference)
if err != nil {
return "", fmt.Sprintf("failed to get local image: %v", err)
}
dst.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: auth.StaticCredential(registry, auth.Credential{
Username: username,
Password: password,
}),
}
if _, err = oras.Copy(context.Background(), src, src.Reference.Reference, dst, "", oras.DefaultCopyOptions); err != nil {
return "", fmt.Sprintf("failed to copy image: %v", err)
}
}
}
return "", ""
}
func findLocalImageManifests(localDir string) ([]string, error) {
var manifests []string
if err := filepath.WalkDir(localDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if path == filepath.Join(localDir, "blobs") {
return filepath.SkipDir
}
if d.IsDir() || filepath.Base(path) == "manifests" {
return nil
}
file, err := os.ReadFile(path)
if err != nil {
return err
}
var data map[string]any
if err := json.Unmarshal(file, &data); err != nil {
return err
}
if data["mediaType"].(string) == imagev1.MediaTypeImageIndex {
subpath, err := filepath.Rel(localDir, path)
if err != nil {
return err
}
// the last dir is manifests. should delete it
manifests = append(manifests, filepath.Dir(filepath.Dir(subpath))+":"+filepath.Base(subpath))
}
return nil
}); err != nil {
return nil, err
}
return manifests, nil
}
func NewLocalRepository(reference string) (*remote.Repository, error) {
ref, err := registry.ParseReference(reference)
if err != nil {
return nil, err
}
return &remote.Repository{
Reference: ref,
Client: &http.Client{Transport: &imageTransport{baseDir: filepath.Join(_const.GetWorkDir(), "kubekey", "images")}},
}, nil
}
var ResponseNotFound = &http.Response{Proto: "Local", StatusCode: http.StatusNotFound}
var ResponseNotAllowed = &http.Response{Proto: "Local", StatusCode: http.StatusMethodNotAllowed}
var ResponseServerError = &http.Response{Proto: "Local", StatusCode: http.StatusInternalServerError}
var ResponseCreated = &http.Response{Proto: "Local", StatusCode: http.StatusCreated}
var ResponseOK = &http.Response{Proto: "Local", StatusCode: http.StatusOK}
const domain = "internal"
const apiPrefix = "/v2/"
type imageTransport struct {
baseDir string
}
func (i imageTransport) RoundTrip(request *http.Request) (*http.Response, error) {
switch request.Method {
case http.MethodHead: // check if file exist
if strings.HasSuffix(filepath.Dir(request.URL.Path), "blobs") { // blobs
filename := filepath.Join(i.baseDir, "blobs", filepath.Base(request.URL.Path))
if _, err := os.Stat(filename); err != nil {
return ResponseNotFound, nil
}
return ResponseOK, nil
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "manifests") { // manifests
filename := filepath.Join(i.baseDir, strings.TrimPrefix(request.URL.Path, apiPrefix))
if _, err := os.Stat(filename); err != nil {
return ResponseNotFound, nil
}
file, err := os.ReadFile(filename)
if err != nil {
return ResponseServerError, err
}
var data map[string]any
if err := json.Unmarshal(file, &data); err != nil {
return ResponseServerError, err
}
return &http.Response{
Proto: "Local",
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{data["mediaType"].(string)},
},
ContentLength: int64(len(file)),
}, nil
}
return ResponseNotAllowed, nil
case http.MethodPost:
if strings.HasSuffix(request.URL.Path, "/uploads/") {
return &http.Response{
Proto: "Local",
StatusCode: http.StatusAccepted,
Header: http.Header{
"Location": []string{filepath.Dir(request.URL.Path)},
},
Request: request,
}, nil
}
return ResponseNotAllowed, nil
case http.MethodPut:
if strings.HasSuffix(request.URL.Path, "/uploads") { // blobs
body, err := io.ReadAll(request.Body)
if err != nil {
return ResponseServerError, nil
}
defer request.Body.Close()
filename := filepath.Join(i.baseDir, "blobs", request.URL.Query().Get("digest"))
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return ResponseServerError, nil
}
if err := os.WriteFile(filename, body, 0644); err != nil {
return ResponseServerError, nil
}
return ResponseCreated, nil
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "/manifests") { // manifest
filename := filepath.Join(i.baseDir, strings.TrimPrefix(request.URL.Path, apiPrefix))
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return ResponseServerError, nil
}
body, err := io.ReadAll(request.Body)
if err != nil {
return ResponseServerError, nil
}
defer request.Body.Close()
if err := os.WriteFile(filename, body, 0644); err != nil {
return ResponseServerError, nil
}
return ResponseCreated, nil
}
return ResponseNotAllowed, nil
case http.MethodGet:
if strings.HasSuffix(filepath.Dir(request.URL.Path), "blobs") { // blobs
filename := filepath.Join(i.baseDir, "blobs", filepath.Base(request.URL.Path))
if _, err := os.Stat(filename); err != nil {
return ResponseNotFound, nil
}
file, err := os.ReadFile(filename)
if err != nil {
return ResponseServerError, err
}
return &http.Response{
Proto: "Local",
StatusCode: http.StatusOK,
ContentLength: int64(len(file)),
Body: io.NopCloser(bytes.NewReader(file)),
}, nil
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "manifests") { // manifests
filename := filepath.Join(i.baseDir, strings.TrimPrefix(request.URL.Path, apiPrefix))
if _, err := os.Stat(filename); err != nil {
return ResponseNotFound, nil
}
file, err := os.ReadFile(filename)
if err != nil {
return ResponseServerError, err
}
var data map[string]any
if err := json.Unmarshal(file, &data); err != nil {
return ResponseServerError, err
}
return &http.Response{
Proto: "Local",
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{data["mediaType"].(string)},
},
ContentLength: int64(len(file)),
Body: io.NopCloser(bytes.NewReader(file)),
}, nil
}
return ResponseNotAllowed, nil
default:
return ResponseNotAllowed, nil
}
}

View File

@ -68,6 +68,7 @@ func init() {
RegisterModule("template", ModuleTemplate)
RegisterModule("set_fact", ModuleSetFact)
RegisterModule("gen_cert", ModuleGenCert)
RegisterModule("image", ModuleImage)
}
// ConnKey for connector which store in context

View File

@ -151,9 +151,27 @@ func StringSliceVar(d map[string]any, vars map[string]any, key string) ([]string
return nil, err
}
var ss []string
if err := json.Unmarshal([]byte(as), &ss); err != nil {
// if is not json format. only return a value contains this
return []string{as}, nil
switch {
case regexp.MustCompile(`^<\[\](.*?) Value>$`).MatchString(as):
// in pongo2 cannot get slice value. add extension filter value.
var input = val.(string)
// try to escape string
if ns, err := strconv.Unquote(val.(string)); err == nil {
input = ns
}
vv := GetValue(d, input)
if _, ok := vv.([]any); ok {
ss = make([]string, len(vv.([]any)))
for i, a := range vv.([]any) {
ss[i] = a.(string)
}
}
default:
// value is simple string
if err := json.Unmarshal([]byte(as), &ss); err != nil {
// if is not json format. only return a value contains this
return []string{as}, nil
}
}
return ss, nil
default: