feat: more type to defined playbook file (#2522)

Signed-off-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
liujian 2025-04-03 15:23:00 +08:00 committed by GitHub
parent 954579beb5
commit 2c19021fb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1164 additions and 1341 deletions

View File

@ -42,13 +42,13 @@ func (c *Config) UnmarshalJSON(data []byte) error {
*c = Config(*aux)
// Decode spec.Raw into spec.Object if it's not already set
objMap := make(map[string]any)
if len(c.Spec.Raw) > 0 && c.Spec.Object == nil {
var objMap map[string]interface{}
if err := json.Unmarshal(c.Spec.Raw, &objMap); err != nil {
return errors.Wrap(err, "failed to unmarshal spec.Raw")
}
c.Spec.Object = &unstructured.Unstructured{Object: objMap}
}
c.Spec.Object = &unstructured.Unstructured{Object: objMap}
return nil
}
@ -71,5 +71,9 @@ func (c *Config) MarshalJSON() ([]byte, error) {
// Value returns the underlying map[string]any from the Config's unstructured Object.
// This provides direct access to the config values stored in Spec.Object.
func (c *Config) Value() map[string]any {
if c.Spec.Object == nil {
return make(map[string]any)
}
return c.Spec.Object.(*unstructured.Unstructured).Object
}

View File

@ -38,8 +38,8 @@ const (
)
const (
// TaskAnnotationRole is the absolute dir of task in project.
TaskAnnotationRole = "kubesphere.io/role"
// TaskAnnotationRelativePath is the relative dir of task in project.
TaskAnnotationRelativePath = "kubesphere.io/rel-path"
)
// TaskSpec of Task

View File

@ -23,7 +23,7 @@
register: cloud_config_out
- name: set_fact of cloud-config value
set_fact:
cloud_config: "{{ .cloud_config_out.stdout | toJson }}"
cloud_config: "{{ .cloud_config_out.stdout | fromYaml | toJson }}"
roles:
- role: init/init-artifacts
when: .kubernetes_installed | default false | eq false

View File

@ -35,4 +35,5 @@ k8s_registry: |
{{- end -}}
cri:
# support: containerd,docker
container_manager: docker

View File

@ -1,9 +1,13 @@
---
- name: Delete Node
command: |
if kubectl get node {{ .hostname }}; then
if kubectl get node {{ .hostname }} > /dev/null 2>&1; then
kubectl cordon {{ .hostname }}
kubectl drain {{ .hostname }} --ignore-daemonsets --delete-emptydir-data --force
if [ $(kubectl get nodes --no-headers | wc -l) -gt 1 ]; then
kubectl drain {{ .hostname }} --ignore-daemonsets --delete-emptydir-data --force --disable-eviction
else
kubectl drain {{ .hostname }} --ignore-daemonsets --delete-emptydir-data --force
fi
{{- if .cni.type | eq "calico" }}
calicoctl delete node {{ .hostname }}
{{- end }}

View File

@ -1,19 +1,21 @@
---
# load defaults vars
- hosts:
- all
vars_files:
- vars/certs_renew.yaml
- import_playbook: hook/pre_install.yaml
- hosts:
- localhost
tags: ["certs"]
vars_files:
- vars/certs_renew.yaml
roles:
- init/init-cert
- hosts:
- etcd
tags: ["certs"]
vars_files:
- vars/certs_renew.yaml
roles:
- role: certs/renew-etcd
when: and (.groups.etcd | default list | len | lt 0) .renew_etcd
@ -21,16 +23,12 @@
- hosts:
- image_registry
tags: ["certs"]
vars_files:
- vars/certs_renew.yaml
roles:
- role: certs/renew-registry
when: and (.groups.image_registry | default list | len | lt 0) .renew_image_registry
- hosts:
- kube_control_plane
vars_files:
- vars/certs_renew.yaml
tags: ["certs"]
roles:
- role: certs/renew-kubernetes

View File

@ -1,4 +1,10 @@
---
# load defaults vars
- hosts:
- all
vars_files:
- vars/create_cluster.yaml
- import_playbook: hook/pre_install.yaml
# precheck
@ -67,8 +73,6 @@
- hosts:
- k8s_cluster
vars_files:
- vars/create_cluster_kubernetes.yaml
gather_facts: true
roles:
- install/cri

View File

@ -5,6 +5,7 @@ kubernetes:
etcd:
deployment_type: external
cri:
# support: containerd,docker
container_manager: docker
image_registry:
type: harbor

View File

@ -1,8 +0,0 @@
cluster_require:
supported_architectures:
amd64:
- amd64
- x86_64
arm64:
- arm64
- aarch64

View File

@ -30,3 +30,13 @@ k8s_registry: |
{{- end -}}
security_enhancement: false
kubernetes:
etcd:
# It is possible to deploy etcd with three methods.
# external: Deploy etcd cluster with external etcd cluster.
# internal: Deploy etcd cluster by static pod.
deployment_type: external
cri:
# support: containerd,docker
container_manager: docker

View File

@ -1,222 +0,0 @@
artifact:
arch: [ "amd64" ]
# offline artifact package for kk.
artifact_file: ""
# the md5_file of artifact_file.
artifact_md5: ""
artifact_url:
etcd:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/etcd/release/download/{{ .etcd_version }}/etcd-{{ .etcd_version }}-linux-amd64.tar.gz
{{- else }}
https://github.com/etcd-io/etcd/releases/download/{{ .etcd_version }}/etcd-{{ .etcd_version }}-linux-amd64.tar.gz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/etcd/release/download/{{ .etcd_version }}/etcd-{{ .etcd_version }}-linux-arm64.tar.gz
{{- else }}
https://github.com/etcd-io/etcd/releases/download/{{ .etcd_version }}/etcd-{{ .etcd_version }}-linux-arm64.tar.gz
{{- end }}
kubeadm:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/amd64/kubeadm
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/amd64/kubeadm
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/arm64/kubeadm
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/arm64/kubeadm
{{- end }}
kubelet:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/amd64/kubelet
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/amd64/kubelet
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/arm64/kubelet
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/arm64/kubelet
{{- end }}
kubectl:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/amd64/kubectl
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/amd64/kubectl
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/release/{{ .kube_version }}/bin/linux/arm64/kubectl
{{- else }}
https://storage.googleapis.com/kubernetes-release/release/{{ .kube_version }}/bin/linux/arm64/kubectl
{{- end }}
cni_plugins:
amd64: |
{{- if .kkzone | eq "cn" }}
https://github.com/containernetworking/plugins/releases/download/{{ .cni_plugins_version }}/cni-plugins-linux-amd64-{{ .cni_plugins_version }}.tgz
{{- else }}
https://containernetworking.pek3b.qingstor.com/plugins/releases/download/{{ .cni_plugins_version }}/cni-plugins-linux-amd64-{{ .cni_plugins_version }}.tgz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://github.com/containernetworking/plugins/releases/download/{{ .cni_plugins_version }}/cni-plugins-linux-arm64-{{ .cni_plugins_version }}.tgz
{{- else }}
https://containernetworking.pek3b.qingstor.com/plugins/releases/download/{{ .cni_plugins_version }}/cni-plugins-linux-arm64-{{ .cni_plugins_version }}.tgz
{{- end }}
helm:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-helm.pek3b.qingstor.com/helm-{{ .helm_version }}-linux-amd64.tar.gz
{{- else }}
https://get.helm.sh/helm-{{ .helm_version }}-linux-amd64.tar.gz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-helm.pek3b.qingstor.com/helm-{{ .helm_version }}-linux-arm64.tar.gz
{{- else }}
https://get.helm.sh/helm-{{ .helm_version }}-linux-arm64.tar.gz
{{- end }}
crictl:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/cri-tools/releases/download/{{ .crictl_version }}/crictl-{{ .crictl_version }}-linux-amd64.tar.gz
{{- else }}
https://github.com/kubernetes-sigs/cri-tools/releases/download/{{ .crictl_version }}/crictl-{{ .crictl_version }}-linux-amd64.tar.gz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/cri-tools/releases/download/{{ .crictl_version }}/crictl-{{ .crictl_version }}-linux-arm64.tar.gz
{{- else }}
https://github.com/kubernetes-sigs/cri-tools/releases/download/{{ .crictl_version }}/crictl-{{ .crictl_version }}-linux-arm64.tar.gz
{{- end }}
docker:
amd64: |
{{- if .kkzone | eq "cn" }}
https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-{{ .docker_version }}.tgz
{{- else }}
https://download.docker.com/linux/static/stable/x86_64/docker-{{ .docker_version }}.tgz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://mirrors.aliyun.com/docker-ce/linux/static/stable/aarch64/docker-{{ .docker_version }}.tgz
{{- else }}
https://download.docker.com/linux/static/stable/aarch64/docker-{{ .docker_version }}.tgz
{{- end }}
cridockerd:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/releases/download/{{ .cridockerd_version }}/cri-dockerd-{{ .cridockerd_version | default "" | trimPrefix "v" }}.amd64.tgz
{{- else }}
https://github.com/Mirantis/cri-dockerd/releases/download/{{ .cridockerd_version }}/cri-dockerd-{{ .cridockerd_version | default "" | trimPrefix "v" }}.amd64.tgz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/releases/download/{{ .cridockerd_version }}/cri-dockerd-{{ .cridockerd_version | default "" | trimPrefix "v" }}.arm64.tgz
{{- else }}
https://github.com/Mirantis/cri-dockerd/releases/download/{{ .cridockerd_version }}/cri-dockerd-{{ .cridockerd_version | default "" | trimPrefix "v" }}.arm64.tgz
{{- end }}
containerd:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/containerd/containerd/releases/download/{{ .containerd_version }}/containerd-{{ .containerd_version | default "" | trimPrefix "v" }}-linux-amd64.tar.gz
{{- else }}
https://github.com/containerd/containerd/releases/download/{{ .containerd_version }}/containerd-{{ .containerd_version | default "" | trimPrefix "v" }}-linux-amd64.tar.gz
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/containerd/containerd/releases/download/{{ .containerd_version }}/containerd-{{ .containerd_version | default "" | trimPrefix "v" }}-linux-arm64.tar.gz
{{- else }}
https://github.com/containerd/containerd/releases/download/{{ .containerd_version }}/containerd-{{ .containerd_version | default "" | trimPrefix "v" }}-linux-arm64.tar.gz
{{- end }}
runc:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/opencontainers/runc/releases/download/{{ .runc_version }}/runc.amd64
{{- else }}
https://github.com/opencontainers/runc/releases/download/{{ .runc_version }}/runc.amd64
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/opencontainers/runc/releases/download/{{ .runc_version }}/runc.arm64
{{- else }}
https://github.com/opencontainers/runc/releases/download/{{ .runc_version }}/runc.arm64
{{- end }}
calicoctl:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/projectcalico/calico/releases/download/{{ .calico_version }}/calicoctl-linux-amd64
{{- else }}
https://github.com/projectcalico/calico/releases/download/{{ .calico_version }}/calicoctl-linux-amd64
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/projectcalico/calico/releases/download/{{ .calico_version }}/calicoctl-linux-arm64
{{- else }}
https://github.com/projectcalico/calico/releases/download/{{ .calico_version }}/calicoctl-linux-arm64
{{- end }}
dockercompose:
amd64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/docker/compose/releases/download/{{ .dockercompose_version }}/docker-compose-linux-x86_64
{{- else }}
https://github.com/docker/compose/releases/download/{{ .dockercompose_version }}/docker-compose-linux-x86_64
{{- end }}
arm64: |
{{- if .kkzone | eq "cn" }}
https://kubernetes-release.pek3b.qingstor.com/docker/compose/releases/download/{{ .dockercompose_version }}/docker-compose-linux-aarch64
{{- else }}
https://github.com/docker/compose/releases/download/{{ .dockercompose_version }}/docker-compose-linux-aarch64
{{- end }}
# registry:
# amd64: |
# {{- if .kkzone | eq "cn" }}
# https://kubernetes-release.pek3b.qingstor.com/registry/{{ .registry_version }}/registry-{{ .registry_version }}-linux-amd64.tgz
# {{- else }}
# https://github.com/kubesphere/kubekey/releases/download/{{ .registry_version }}/registry-{{ .registry_version }}-linux-amd64.tgz
# {{- end }}
# arm64: |
# {{- if .kkzone | eq "cn" }}
# https://kubernetes-release.pek3b.qingstor.com/registry/{{ .registry_version }}/registry-{{ .registry_version }}-linux-arm64.tgz
# {{- else }}
# https://github.com/kubesphere/kubekey/releases/download/{{ .registry_version }}/registry-{{ .registry_version }}-linux-arm64.tgz
# {{- end }}
harbor:
amd64: |
{{- if .kkzone | eq "cn" }}
https://github.com/goharbor/harbor/releases/download/{{ .harbor_version }}/harbor-offline-installer-{{ .harbor_version }}.tgz
{{- else }}
https://github.com/goharbor/harbor/releases/download/{{ .harbor_version }}/harbor-offline-installer-{{ .harbor_version }}.tgz
{{- end }}
# arm64: |
# {{- if .kkzone | eq "cn" }}
# https://github.com/goharbor/harbor/releases/download/{{ .harbor_version }}/harbor-{{ .harbor_version }}-linux-arm64.tgz
# {{- else }}
# https://github.com/goharbor/harbor/releases/download/{{ .harbor_version }}/harbor-{{ .harbor_version }}-linux-arm64.tgz
# {{- end }}
# keepalived:
# amd64: |
# {{- if .kkzone | eq "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
# {{- end }}
# arm64: |
# {{- if .kkzone | eq "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
# {{- end }}
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
nfs_provisioner: https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/releases/download/nfs-subdir-external-provisioner-4.0.18/nfs-subdir-external-provisioner-{{ .nfs_provisioner_version }}.tgz
images:
auth: []
list: []

View File

@ -3,8 +3,8 @@ cri:
cgroup_driver: systemd
sandbox_image: |
{{ .k8s_registry }}/pause:3.5
# support: containerd,docker,crio
container_manager: docker
# support: containerd,docker
# container_manager: docker
# the endpoint of containerd
cri_socket: |
{{- if .cri.container_manager | eq "containerd" }}

View File

@ -190,7 +190,7 @@ kubernetes:
# It is possible to deploy etcd with three methods.
# external: Deploy etcd cluster with external etcd cluster.
# internal: Deploy etcd cluster by static pod.
deployment_type: external
# deployment_type: external
image: |
{{ .k8s_registry }}/etcd:3.5.0
custom_label: {}

View File

@ -28,4 +28,4 @@ cluster_require:
arm64:
- arm64
- aarch64
min_kernel_version: 4.9.17
min_kernel_version: 4.9.17

View File

@ -7,9 +7,9 @@
run_once: true
when: and .kubernetes.etcd.deployment_type (ne .kubernetes.etcd.deployment_type "")
- name: Stop if etcd group is empty in internal etcd mode
- name: Stop if etcd group is empty in external etcd mode
assert:
that: .groups.etcd
that: .groups.etcd | len | lt 0
fail_msg: "group \"etcd\" cannot be empty in external etcd mode"
run_once: true
when: .kubernetes.etcd.deployment_type | eq "external"
@ -18,7 +18,7 @@
assert:
that: (mod (.groups.etcd | len) 2) | eq 1
fail_msg: "etcd number should be odd number"
when: .groups.etcd
when: .kubernetes.etcd.deployment_type | eq "external"
## https://cwiki.yunify.com/pages/viewpage.action?pageId=145920824
- name: Check dev io for etcd

View File

@ -34,7 +34,7 @@ import (
const (
defaultKubeVersion = "v1.23.15"
defaultContainerManager = "containerd"
defaultContainerManager = "docker"
)
// NewCreateClusterOptions for newCreateClusterCommand

View File

@ -79,13 +79,11 @@ const ProjectRolesDir = "roles"
const ProjectRolesTasksDir = "tasks"
// ProjectRolesTasksMainFile is a mandatory file under the tasks directory that must be executed when the role is run. It supports files with .yaml or .yml extensions.
const ProjectRolesTasksMainFile = "main"
// ProjectRolesDefaultsDir is a fixed directory name under a role, used to set default variables for the role.
const ProjectRolesDefaultsDir = "defaults"
// ProjectRolesDefaultsMainFile is a mandatory file under the defaults directory. It supports files with .yaml or .yml extensions.
const ProjectRolesDefaultsMainFile = "main"
// ProjectRolesTemplateDir is a fixed directory name under a role, used to store templates required by tasks.
const ProjectRolesTemplateDir = "templates"

View File

@ -13,6 +13,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection/resourcelock"
@ -384,16 +385,20 @@ func (r *KKMachineReconciler) getConfig(scope *clusterScope, kkmachine *capkkinf
if err := unstructured.SetNestedField(config.Value(), _const.ProviderID2Host(scope.Name, kkmachine.Spec.ProviderID), "node_name"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "node_name")
}
if err := unstructured.SetNestedField(config.Value(), kkmachine.Spec.Version, "kube_version"); err != nil {
if err := unstructured.SetNestedField(config.Value(), *kkmachine.Spec.Version, "kube_version"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "kube_version")
}
if err := unstructured.SetNestedField(config.Value(), scope.Cluster.Name, "kubernetes", "cluster_name"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "kubernetes.cluster_name")
}
if err := unstructured.SetNestedField(config.Value(), kkmachine.Spec.Roles, "kubernetes", "roles"); err != nil {
if err := unstructured.SetNestedStringSlice(config.Value(), kkmachine.Spec.Roles, "kubernetes", "roles"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "kubernetes.roles")
}
if err := unstructured.SetNestedField(config.Value(), scope.Cluster.Spec.ClusterNetwork, "cluster_network"); err != nil {
converted, err := runtime.DefaultUnstructuredConverter.ToUnstructured(scope.Cluster.Spec.ClusterNetwork)
if err != nil {
return config, errors.Wrap(err, "failed to convert scope.Cluster.Spec.ClusterNetwork")
}
if err := unstructured.SetNestedMap(config.Value(), converted, "cluster_network"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "cluster_network")
}
@ -411,7 +416,7 @@ func (r *KKMachineReconciler) getConfig(scope *clusterScope, kkmachine *capkkinf
if err := unstructured.SetNestedField(config.Value(), scope.Cluster.Spec.ControlPlaneEndpoint.Host, "kubernetes", "control_plane_endpoint", "host"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "kubernetes.control_plane_endpoint.host")
}
if err := unstructured.SetNestedField(config.Value(), scope.KKCluster.Spec.ControlPlaneEndpointType, "kubernetes", "control_plane_endpoint", "type"); err != nil {
if err := unstructured.SetNestedField(config.Value(), string(scope.KKCluster.Spec.ControlPlaneEndpointType), "kubernetes", "control_plane_endpoint", "type"); err != nil {
return config, errors.Wrapf(err, "failed to set %q in config", "kubernetes.control_plane_endpoint.kube_vip.type")
}

View File

@ -36,7 +36,7 @@ import (
)
// MarshalBlock marshal block to task
func MarshalBlock(role string, hosts []string, when []string, block kkprojectv1.Block) *kkcorev1alpha1.Task {
func MarshalBlock(hosts []string, when []string, block kkprojectv1.Block) *kkcorev1alpha1.Task {
task := &kkcorev1alpha1.Task{
TypeMeta: metav1.TypeMeta{
Kind: "Task",
@ -44,9 +44,6 @@ func MarshalBlock(role string, hosts []string, when []string, block kkprojectv1.
},
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: metav1.Now(),
Annotations: map[string]string{
kkcorev1alpha1.TaskAnnotationRole: role,
},
},
Spec: kkcorev1alpha1.TaskSpec{
Name: block.Name,
@ -58,6 +55,9 @@ func MarshalBlock(role string, hosts []string, when []string, block kkprojectv1.
Register: block.Register,
},
}
if annotation, ok := block.UnknownField["annotations"].(map[string]string); ok {
task.ObjectMeta.Annotations = annotation
}
if block.Loop != nil {
data, err := json.Marshal(block.Loop)

View File

@ -22,6 +22,7 @@ func funcMap() template.FuncMap {
delete(f, "expandenv")
// add custom function
f["toYaml"] = toYAML
f["fromYaml"] = fromYAML
f["ipInCIDR"] = ipInCIDR
f["ipFamily"] = ipFamily
f["pow"] = pow
@ -43,6 +44,15 @@ func toYAML(v any) string {
return strings.TrimSpace(string(data))
}
// fromYAML takes a YAML string, unmarshals it into an interface{}, and returns the result.
// If there is an error during unmarshaling, it will be returned along with nil for the value.
func fromYAML(v string) (any, error) {
var output any
err := yaml.Unmarshal([]byte(v), &output)
return output, err
}
// ipInCIDR get the IP of a specific location within the cidr range
func ipInCIDR(index int, cidr string) (string, error) {
var ips = make([]string, 0)

View File

@ -525,6 +525,18 @@ func TestParseFunction(t *testing.T) {
},
excepted: []byte("a1: b1\na2: b2"),
},
// ======= fromYaml =======
{
name: "fromYaml 1",
input: "{{ .foo | fromYaml | toJson }}",
variable: map[string]any{
"foo": `
a1: b1
a2:
b2: 1`,
},
excepted: []byte("{\"a1\":\"b1\",\"a2\":{\"b2\":1}}"),
},
// ======= indent =======
{
name: "indent 1",

View File

@ -158,7 +158,7 @@ func (e blockExecutor) dealBlock(ctx context.Context, hosts []string, ignoreErro
// dealTask "block" argument is not defined in block.
func (e blockExecutor) dealTask(ctx context.Context, hosts []string, when []string, block kkprojectv1.Block) error {
task := converter.MarshalBlock(e.role, hosts, when, block)
task := converter.MarshalBlock(hosts, when, block)
// complete module by unknown field
for n, a := range block.UnknownField {
data, err := json.Marshal(a)

View File

@ -38,7 +38,7 @@ func NewPlaybookExecutor(ctx context.Context, client ctrlclient.Client, playbook
// get variable
v, err := variable.New(ctx, client, *playbook, source.FileSource)
if err != nil {
klog.V(5).ErrorS(nil, "convert playbook error", "playbook", ctrlclient.ObjectKeyFromObject(playbook))
klog.V(5).ErrorS(err, "get variable error", "playbook", ctrlclient.ObjectKeyFromObject(playbook))
return nil
}

View File

@ -13,7 +13,6 @@ import (
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
"github.com/schollz/progressbar/v3"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
@ -85,8 +84,8 @@ func (e *taskExecutor) runTaskLoop(ctx context.Context) error {
// Add role prefix to log output if role annotation exists
var roleLog string
if e.task.Annotations[kkcorev1alpha1.TaskAnnotationRole] != "" {
roleLog = "[" + e.task.Annotations[kkcorev1alpha1.TaskAnnotationRole] + "] "
if e.task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath] != "" {
roleLog = "[" + e.task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath] + "] "
}
fmt.Fprintf(e.logOutput, "%s %s%s\n", time.Now().Format(time.TimeOnly+" MST"), roleLog, e.task.Spec.Name)
@ -241,7 +240,8 @@ func (e *taskExecutor) execTaskHostLogs(ctx context.Context, h string, stdout, s
// progress bar for task
var bar = progressbar.NewOptions(-1,
progressbar.OptionSetWriter(e.logOutput),
progressbar.OptionSpinnerCustom([]string{" "}),
// progressbar.OptionSpinnerCustom([]string{" "}),
progressbar.OptionSpinnerType(14),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionSetDescription(fmt.Sprintf("[\033[36m%s\033[0m]%s \033[36mrunning\033[0m", h, placeholder)),
progressbar.OptionOnCompletion(func() {
@ -377,13 +377,10 @@ func (e *taskExecutor) dealRegister(stdout, stderr, host string) error {
if e.task.Spec.Register != "" {
var stdoutResult any = stdout
var stderrResult any = stderr
// try to convert by json or yaml
// try to convert by json
if json.Valid([]byte(stdout)) {
_ = json.Unmarshal([]byte(stdout), &stdoutResult)
_ = json.Unmarshal([]byte(stderr), &stderrResult)
} else {
_ = yaml.Unmarshal([]byte(stdout), &stdoutResult)
_ = yaml.Unmarshal([]byte(stderr), &stderrResult)
}
// set variable to parent location
if err := e.variable.Merge(variable.MergeRuntimeVariable(map[string]any{

View File

@ -32,6 +32,7 @@ import (
"k8s.io/utils/ptr"
"github.com/kubesphere/kubekey/v4/pkg/connector"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/project"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
@ -98,57 +99,106 @@ func ModuleCopy(ctx context.Context, options ExecOptions) (string, string) {
// copySrc copy src file to dest
func (ca copyArgs) copySrc(ctx context.Context, options ExecOptions, conn connector.Connector) (string, string) {
dealAbsoluteFilePath := func() (string, string) {
fileInfo, err := os.Stat(ca.src)
if err != nil {
return "", fmt.Sprintf(" get src file %s in local path error: %v", ca.src, err)
}
if fileInfo.IsDir() { // src is dir
if err := ca.absDir(ctx, conn); err != nil {
return "", fmt.Sprintf("sync copy absolute dir error %s", err)
}
} else { // src is file
data, err := os.ReadFile(ca.src)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ca.readFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return "", fmt.Sprintf("sync copy absolute dir error %s", err)
}
}
return StdoutSuccess, ""
}
dealRelativeFilePath := func() (string, string) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return "", fmt.Sprintf("get project error: %v", err)
}
fileInfo, err := pj.Stat(ca.src, project.GetFileOption{IsFile: true, Role: options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole]})
if err != nil {
return "", fmt.Sprintf("get file %s from project error %v", ca.src, err)
}
if fileInfo.IsDir() {
if err := ca.relDir(ctx, pj, options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole], conn); err != nil {
return "", fmt.Sprintf("sync copy relative dir error %s", err)
}
} else {
data, err := pj.ReadFile(ca.src, project.GetFileOption{IsFile: true, Role: options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole]})
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ca.readFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return "", fmt.Sprintf("sync copy relative dir error %s", err)
}
}
return StdoutSuccess, ""
}
if filepath.IsAbs(ca.src) { // if src is absolute path. find it in local path
return dealAbsoluteFilePath()
return ca.handleAbsolutePath(ctx, conn)
}
// if src is not absolute path. find file in project
return dealRelativeFilePath()
return ca.handleRelativePath(ctx, options, conn)
}
func (ca copyArgs) handleAbsolutePath(ctx context.Context, conn connector.Connector) (string, string) {
fileInfo, err := os.Stat(ca.src)
if err != nil {
return "", fmt.Sprintf(" get src file %s in local path error: %v", ca.src, err)
}
if fileInfo.IsDir() { // src is dir
if err := ca.absDir(ctx, conn); err != nil {
return "", fmt.Sprintf("sync copy absolute dir error %s", err)
}
return StdoutSuccess, ""
}
// src is file
data, err := os.ReadFile(ca.src)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ca.readFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return "", fmt.Sprintf("sync copy absolute dir error %s", err)
}
return StdoutSuccess, ""
}
func (ca copyArgs) handleRelativePath(ctx context.Context, options ExecOptions, conn connector.Connector) (string, string) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return "", fmt.Sprintf("get project error: %v", err)
}
relPath := filepath.Join(options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], _const.ProjectRolesFilesDir, ca.src)
fileInfo, err := pj.Stat(relPath)
if err != nil {
return "", fmt.Sprintf("get file %s from project error %v", ca.src, err)
}
if fileInfo.IsDir() {
if err := ca.handleRelativeDir(ctx, pj, relPath, conn); err != nil {
return "", fmt.Sprintf("sync copy relative dir error %s", err)
}
return StdoutSuccess, ""
}
// Handle single file
data, err := pj.ReadFile(relPath)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ca.readFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return "", fmt.Sprintf("sync copy relative dir error %s", err)
}
return StdoutSuccess, ""
}
func (ca copyArgs) handleRelativeDir(ctx context.Context, pj project.Project, relPath string, conn connector.Connector) error {
return pj.WalkDir(relPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() { // only copy file
return nil
}
info, err := d.Info()
if err != nil {
return errors.Wrap(err, "failed to get file info")
}
mode := info.Mode()
if ca.mode != nil {
mode = os.FileMode(*ca.mode)
}
data, err := pj.ReadFile(path)
if err != nil {
return errors.Wrap(err, "failed to read file")
}
dest := ca.dest
if strings.HasSuffix(ca.dest, "/") {
rel, err := pj.Rel(relPath, path)
if err != nil {
return errors.Wrap(err, "failed to get relative file path")
}
dest = filepath.Join(ca.dest, rel)
}
return conn.PutFile(ctx, data, dest, mode)
})
}
// copyContent convert content param and copy to dest
@ -168,44 +218,6 @@ func (ca copyArgs) copyContent(ctx context.Context, mode fs.FileMode, conn conne
return StdoutSuccess, ""
}
// relDir when copy.src is relative dir, get all files from project, and copy to remote.
func (ca copyArgs) relDir(ctx context.Context, pj project.Project, role string, conn connector.Connector) error {
return pj.WalkDir(ca.src, project.GetFileOption{IsFile: true, Role: role}, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() { // only copy file
return nil
}
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return errors.Wrap(err, "failed to get file info")
}
mode := info.Mode()
if ca.mode != nil {
mode = os.FileMode(*ca.mode)
}
data, err := pj.ReadFile(path, project.GetFileOption{Role: role})
if err != nil {
return errors.Wrap(err, "failed to read file")
}
dest := ca.dest
if strings.HasSuffix(ca.dest, "/") {
rel, err := pj.Rel(ca.src, path, project.GetFileOption{Role: role})
if err != nil {
return errors.Wrap(err, "failed to get relative file path")
}
dest = filepath.Join(ca.dest, rel)
}
return conn.PutFile(ctx, data, dest, mode)
})
}
// absFile when copy.src is absolute file, get file from os, and copy to remote.
func (ca copyArgs) readFile(ctx context.Context, data []byte, mode fs.FileMode, conn connector.Connector) error {
dest := ca.dest

View File

@ -32,6 +32,7 @@ import (
"k8s.io/utils/ptr"
"github.com/kubesphere/kubekey/v4/pkg/connector"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/converter/tmpl"
"github.com/kubesphere/kubekey/v4/pkg/project"
"github.com/kubesphere/kubekey/v4/pkg/variable"
@ -74,78 +75,132 @@ func newTemplateArgs(_ context.Context, raw runtime.RawExtension, vars map[strin
// ModuleTemplate deal "template" module
func ModuleTemplate(ctx context.Context, options ExecOptions) (string, string) {
// get host variable
ha, err := options.getAllVariables()
if err != nil {
return "", err.Error()
}
ta, err := newTemplateArgs(ctx, options.Args, ha)
if err != nil {
return "", err.Error()
}
// get connector
conn, err := getConnector(ctx, options.Host, options.Variable)
ha, ta, conn, err := prepareTemplate(ctx, options)
if err != nil {
return "", err.Error()
}
defer conn.Close(ctx)
dealAbsoluteFilePath := func() (string, string) {
fileInfo, err := os.Stat(ta.src)
if err != nil {
return "", fmt.Sprintf(" get src file %s in local path error: %v", ta.src, err)
}
if fileInfo.IsDir() { // src is dir
if err := ta.absDir(ctx, conn, ha); err != nil {
return "", fmt.Sprintf("sync template absolute dir error %s", err)
}
} else { // src is file
data, err := os.ReadFile(ta.src)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, ha); err != nil {
return "", fmt.Sprintf("sync template absolute file error %s", err)
}
}
return StdoutSuccess, ""
}
dealRelativeFilePath := func() (string, string) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return "", fmt.Sprintf("get project error: %v", err)
}
fileInfo, err := pj.Stat(ta.src, project.GetFileOption{IsTemplate: true, Role: options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole]})
if err != nil {
return "", fmt.Sprintf("get file %s from project error: %v", ta.src, err)
}
if fileInfo.IsDir() {
if err := ta.relDir(ctx, pj, options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole], conn, ha); err != nil {
return "", fmt.Sprintf("sync template relative dir error: %s", err)
}
} else {
data, err := pj.ReadFile(ta.src, project.GetFileOption{IsTemplate: true, Role: options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRole]})
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, ha); err != nil {
return "", fmt.Sprintf("sync template relative dir error: %s", err)
}
}
return StdoutSuccess, ""
}
if filepath.IsAbs(ta.src) {
return dealAbsoluteFilePath()
return handleAbsoluteTemplate(ctx, ta, conn, ha)
}
return dealRelativeFilePath()
return handleRelativeTemplate(ctx, ta, conn, ha, options)
}
func prepareTemplate(ctx context.Context, options ExecOptions) (map[string]any, *templateArgs, connector.Connector, error) {
ha, err := options.getAllVariables()
if err != nil {
return nil, nil, nil, err
}
ta, err := newTemplateArgs(ctx, options.Args, ha)
if err != nil {
return nil, nil, nil, err
}
conn, err := getConnector(ctx, options.Host, options.Variable)
if err != nil {
return nil, nil, nil, err
}
return ha, ta, conn, nil
}
func handleAbsoluteTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any) (string, string) {
fileInfo, err := os.Stat(ta.src)
if err != nil {
return "", fmt.Sprintf(" get src file %s in local path error: %v", ta.src, err)
}
if fileInfo.IsDir() {
if err := ta.absDir(ctx, conn, vars); err != nil {
return "", fmt.Sprintf("sync template absolute dir error %s", err)
}
return StdoutSuccess, ""
}
data, err := os.ReadFile(ta.src)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
return "", fmt.Sprintf("sync template absolute file error %s", err)
}
return StdoutSuccess, ""
}
func handleRelativeTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any, options ExecOptions) (string, string) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return "", fmt.Sprintf("get project error: %v", err)
}
relPath := filepath.Join(options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], _const.ProjectRolesTemplateDir, ta.src)
fileInfo, err := pj.Stat(relPath)
if err != nil {
return "", fmt.Sprintf("get file %s from project error: %v", ta.src, err)
}
if fileInfo.IsDir() {
if err := handleRelativeDir(ctx, pj, relPath, ta, conn, vars); err != nil {
return "", fmt.Sprintf("sync template relative dir error: %s", err)
}
return StdoutSuccess, ""
}
data, err := pj.ReadFile(relPath)
if err != nil {
return "", fmt.Sprintf("read file error: %s", err)
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
return "", fmt.Sprintf("sync template relative dir error: %s", err)
}
return StdoutSuccess, ""
}
func handleRelativeDir(ctx context.Context, pj project.Project, relPath string, ta *templateArgs, conn connector.Connector, vars map[string]any) error {
return pj.WalkDir(relPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() { // only deal file
return nil
}
info, err := d.Info()
if err != nil {
return errors.Wrapf(err, "failed to get file %q info", path)
}
mode := info.Mode()
if ta.mode != nil {
mode = os.FileMode(*ta.mode)
}
data, err := pj.ReadFile(path)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", path)
}
result, err := tmpl.Parse(vars, string(data))
if err != nil {
return errors.Wrapf(err, "failed to parse file %q", path)
}
dest := ta.dest
if strings.HasSuffix(ta.dest, "/") {
rel, err := pj.Rel(relPath, path)
if err != nil {
return errors.Wrap(err, "failed to get relative filepath")
}
dest = filepath.Join(ta.dest, rel)
}
return conn.PutFile(ctx, result, dest, mode)
})
}
// relFile when template.src is relative file, get file from project, parse it, and copy to remote.
@ -167,48 +222,6 @@ func (ta templateArgs) readFile(ctx context.Context, data string, mode fs.FileMo
return conn.PutFile(ctx, result, dest, mode)
}
// relDir when template.src is relative dir, get all files from project, parse it, and copy to remote.
func (ta templateArgs) relDir(ctx context.Context, pj project.Project, role string, conn connector.Connector, vars map[string]any) error {
return pj.WalkDir(ta.src, project.GetFileOption{IsTemplate: true, Role: role}, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() { // only copy file
return nil
}
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return errors.Wrapf(err, "failed to get file %q info", path)
}
mode := info.Mode()
if ta.mode != nil {
mode = os.FileMode(*ta.mode)
}
data, err := pj.ReadFile(path, project.GetFileOption{IsTemplate: true, Role: role})
if err != nil {
return errors.Wrapf(err, "failed to read file %q", path)
}
result, err := tmpl.Parse(vars, string(data))
if err != nil {
return errors.Wrapf(err, "failed to parse file %q", path)
}
dest := ta.dest
if strings.HasSuffix(ta.dest, "/") {
rel, err := pj.Rel(ta.src, path, project.GetFileOption{IsTemplate: true, Role: role})
if err != nil {
return errors.Wrap(err, "failed to get relative filepath")
}
dest = filepath.Join(ta.dest, rel)
}
return conn.PutFile(ctx, result, dest, mode)
})
}
// absDir when template.src is absolute dir, get all files by os, parse it, and copy to remote.
func (ta templateArgs) absDir(ctx context.Context, conn connector.Connector, vars map[string]any) error {
if err := filepath.WalkDir(ta.src, func(path string, d fs.DirEntry, err error) error {

View File

@ -20,8 +20,6 @@ limitations under the License.
package project
import (
"io/fs"
"os"
"path/filepath"
"github.com/cockroachdb/errors"
@ -29,7 +27,6 @@ import (
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
"github.com/kubesphere/kubekey/v4/builtin/core"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
)
func init() {
@ -42,70 +39,10 @@ func init() {
return nil, errors.New("playbook should be relative path base on project.addr")
}
return &builtinProject{Playbook: playbook, FS: core.BuiltinPlaybook, playbook: playbook.Spec.Playbook}, nil
return &project{
FS: core.BuiltinPlaybook,
basePlaybook: playbook.Spec.Playbook,
Playbook: &kkprojectv1.Playbook{},
}, nil
}
}
type builtinProject struct {
kkcorev1.Playbook
fs.FS
// playbook relpath base on projectDir
playbook string
}
func (p builtinProject) getFilePath(path string, o GetFileOption) string {
var find []string
switch {
case o.IsFile:
if o.Role != "" {
// find from project/roles/roleName
find = append(find, filepath.Join(_const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
}
find = append(find, filepath.Join(_const.ProjectRolesFilesDir, path))
case o.IsTemplate:
// find from project/roles/roleName
if o.Role != "" {
find = append(find, filepath.Join(_const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
}
find = append(find, filepath.Join(_const.ProjectRolesTemplateDir, path))
default:
find = append(find, path)
}
for _, s := range find {
if _, err := fs.Stat(p.FS, s); err == nil {
return s
}
}
return ""
}
// MarshalPlaybook project file to playbook.
func (p builtinProject) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
return marshalPlaybook(p.FS, p.playbook)
}
// Stat role/file/template file or dir in project
func (p builtinProject) Stat(path string, option GetFileOption) (os.FileInfo, error) {
return fs.Stat(p.FS, p.getFilePath(path, option))
}
// WalkDir role/file/template dir in project
func (p builtinProject) WalkDir(path string, option GetFileOption, f fs.WalkDirFunc) error {
return fs.WalkDir(p.FS, p.getFilePath(path, option), f)
}
// ReadFile role/file/template file or dir in project
func (p builtinProject) ReadFile(path string, option GetFileOption) ([]byte, error) {
return fs.ReadFile(p.FS, p.getFilePath(path, option))
}
// Rel path for role/file/template file or dir in project
func (p builtinProject) Rel(root string, path string, option GetFileOption) (string, error) {
return filepath.Rel(p.getFilePath(root, option), path)
}

View File

@ -18,7 +18,6 @@ package project
import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"
@ -48,134 +47,65 @@ func newGitProject(ctx context.Context, playbook kkcorev1.Playbook, update bool)
if err != nil {
return nil, errors.Wrapf(err, "failed to get %q in config", _const.ProjectsDir)
}
// git clone to project dir
if playbook.Spec.Project.Name == "" {
playbook.Spec.Project.Name = strings.TrimSuffix(playbook.Spec.Project.Addr[strings.LastIndex(playbook.Spec.Project.Addr, "/")+1:], ".git")
}
p := &gitProject{
Playbook: playbook,
projectDir: filepath.Join(projectDir, playbook.Spec.Project.Name),
playbook: playbook.Spec.Playbook,
}
if _, err := os.Stat(p.projectDir); os.IsNotExist(err) {
projectDir = filepath.Join(projectDir, playbook.Spec.Project.Name)
if _, err := os.Stat(projectDir); os.IsNotExist(err) {
// git clone
if err := p.gitClone(ctx); err != nil {
if err := gitClone(ctx, projectDir, playbook.Spec.Project); err != nil {
return nil, err
}
} else if update {
// git pull
if err := p.gitPull(ctx); err != nil {
if err := gitPull(ctx, projectDir, playbook.Spec.Project); err != nil {
return nil, err
}
}
return p, nil
return &project{
FS: os.DirFS(filepath.Join(projectDir, playbook.Spec.Project.Name)),
basePlaybook: playbook.Spec.Playbook,
Playbook: &kkprojectv1.Playbook{},
}, nil
}
// gitProject from git
type gitProject struct {
kkcorev1.Playbook
//location
projectDir string
// playbook relpath base on projectDir
playbook string
}
func (p gitProject) getFilePath(path string, o GetFileOption) string {
var find []string
switch {
case o.IsFile:
if o.Role != "" {
// find from project/roles/roleName
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.projectDir, p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
}
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesFilesDir, path))
case o.IsTemplate:
// find from project/roles/roleName
if o.Role != "" {
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.projectDir, p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
}
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesTemplateDir, path))
default:
find = append(find, filepath.Join(p.projectDir, path))
}
for _, s := range find {
if _, err := os.Stat(s); err == nil {
return s
}
}
return ""
}
func (p gitProject) gitClone(ctx context.Context) error {
if _, err := git.PlainCloneContext(ctx, p.projectDir, false, &git.CloneOptions{
URL: p.Playbook.Spec.Project.Addr,
func gitClone(ctx context.Context, localDir string, project kkcorev1.PlaybookProject) error {
if _, err := git.PlainCloneContext(ctx, localDir, false, &git.CloneOptions{
URL: project.Addr,
Progress: nil,
ReferenceName: plumbing.NewBranchReferenceName(p.Playbook.Spec.Project.Branch),
ReferenceName: plumbing.NewBranchReferenceName(project.Branch),
SingleBranch: true,
Auth: &http.TokenAuth{Token: p.Playbook.Spec.Project.Token},
Auth: &http.TokenAuth{Token: project.Token},
InsecureSkipTLS: false,
}); err != nil {
return errors.Wrapf(err, "failed to clone project %q", p.Playbook.Spec.Project.Addr)
return errors.Wrapf(err, "failed to clone project %q", project.Addr)
}
return nil
}
func (p gitProject) gitPull(ctx context.Context) error {
open, err := git.PlainOpen(p.projectDir)
func gitPull(ctx context.Context, localDir string, project kkcorev1.PlaybookProject) error {
open, err := git.PlainOpen(localDir)
if err != nil {
return errors.Wrapf(err, "failed to open git project %a", p.projectDir)
return errors.Wrapf(err, "failed to open git project %a", localDir)
}
wt, err := open.Worktree()
if err != nil {
return errors.Wrapf(err, "failed to open git project %q worktree", p.projectDir)
return errors.Wrapf(err, "failed to open git project %q worktree", localDir)
}
if err := wt.PullContext(ctx, &git.PullOptions{
RemoteURL: p.Playbook.Spec.Project.Addr,
ReferenceName: plumbing.NewBranchReferenceName(p.Playbook.Spec.Project.Branch),
RemoteURL: project.Addr,
ReferenceName: plumbing.NewBranchReferenceName(project.Branch),
SingleBranch: true,
Auth: &http.TokenAuth{Token: p.Playbook.Spec.Project.Token},
Auth: &http.TokenAuth{Token: project.Token},
InsecureSkipTLS: false,
}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return errors.Wrapf(err, "failed to pull git project %q", p.playbook)
return errors.Wrapf(err, "failed to pull git project %q", project.Addr)
}
return nil
}
// MarshalPlaybook project file to playbook.
func (p gitProject) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
return marshalPlaybook(os.DirFS(p.projectDir), p.Playbook.Spec.Playbook)
}
// Stat role/file/template file or dir in project
func (p gitProject) Stat(path string, option GetFileOption) (os.FileInfo, error) {
return os.Stat(p.getFilePath(path, option))
}
// WalkDir role/file/template dir in project
func (p gitProject) WalkDir(path string, option GetFileOption, f fs.WalkDirFunc) error {
return filepath.WalkDir(p.getFilePath(path, option), f)
}
// ReadFile role/file/template file or dir in project
func (p gitProject) ReadFile(path string, option GetFileOption) ([]byte, error) {
return os.ReadFile(p.getFilePath(path, option))
}
// Rel path for role/file/template file or dir in project
func (p gitProject) Rel(root string, path string, option GetFileOption) (string, error) {
return filepath.Rel(p.getFilePath(root, option), path)
}

View File

@ -1,347 +0,0 @@
/*
Copyright 2023 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 project
import (
"io/fs"
"os"
"path/filepath"
"github.com/cockroachdb/errors"
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
"gopkg.in/yaml.v3"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
// marshalPlaybook kkprojectv1.Playbook from a playbook file
func marshalPlaybook(baseFS fs.FS, pbPath string) (*kkprojectv1.Playbook, error) {
// convert playbook to kkprojectv1.Playbook
pb := &kkprojectv1.Playbook{}
if err := loadPlaybook(baseFS, pbPath, pb); err != nil {
return nil, err
}
// convertRoles.
if err := convertRoles(baseFS, pbPath, pb); err != nil {
return nil, err
}
// convertIncludeTasks
if err := convertIncludeTasks(baseFS, pbPath, pb); err != nil {
return nil, err
}
// validate playbook
if err := pb.Validate(); err != nil {
return nil, err
}
return pb, nil
}
// loadPlaybook with include_playbook. Join all playbooks into one playbook
func loadPlaybook(baseFS fs.FS, pbPath string, pb *kkprojectv1.Playbook) error {
// baseDir is the local ansible project dir which playbook belong to
pbData, err := fs.ReadFile(baseFS, pbPath)
if err != nil {
return errors.Wrapf(err, "failed to read playbook %q", pbPath)
}
var plays []kkprojectv1.Play
if err := yaml.Unmarshal(pbData, &plays); err != nil {
return errors.Wrapf(err, "failed to unmarshal playbook %q", pbPath)
}
for _, p := range plays {
if err := dealImportPlaybook(p, baseFS, pbPath, pb); err != nil {
return err
}
if err := dealVarsFiles(&p, baseFS, pbPath); err != nil {
return err
}
// fill block in roles
if err := dealRoles(p, baseFS, pbPath); err != nil {
return err
}
pb.Play = append(pb.Play, p)
}
return nil
}
// dealImportPlaybook "import_playbook" argument in play
func dealImportPlaybook(p kkprojectv1.Play, baseFS fs.FS, pbPath string, pb *kkprojectv1.Playbook) error {
if p.ImportPlaybook != "" {
importPlaybook := getPlaybookBaseFromPlaybook(baseFS, pbPath, p.ImportPlaybook)
if importPlaybook == "" {
return errors.Errorf("import_playbook %q path is empty, it's maybe [project-dir/playbooks/import_playbook_file, playbook-dir/playbooks/import_playbook-file, playbook-dir/import_playbook-file]", p.ImportPlaybook)
}
if err := loadPlaybook(baseFS, importPlaybook, pb); err != nil {
return err
}
}
return nil
}
// dealVarsFiles "var_files" argument in play
func dealVarsFiles(p *kkprojectv1.Play, baseFS fs.FS, pbPath string) error {
for _, file := range p.VarsFiles {
// load vars from vars_files
if _, err := fs.Stat(baseFS, filepath.Join(filepath.Dir(pbPath), file)); err != nil {
return errors.Wrapf(err, "failed to stat file %q", file)
}
data, err := fs.ReadFile(baseFS, filepath.Join(filepath.Dir(pbPath), file))
if err != nil {
return errors.Wrapf(err, "failed to read file %q", filepath.Join(filepath.Dir(pbPath), file))
}
var newVars map[string]any
// Unmarshal the YAML document into a root node.
if err := yaml.Unmarshal(data, &newVars); err != nil {
return errors.Wrap(err, "failed to failed to unmarshal YAML")
}
// store vars in play. the vars defined in file should not be repeated.
p.Vars = variable.CombineVariables(newVars, p.Vars)
}
return nil
}
// dealRoles "roles" argument in play
func dealRoles(p kkprojectv1.Play, baseFS fs.FS, pbPath string) error {
for i, r := range p.Roles {
roleBase := getRoleBaseFromPlaybook(baseFS, pbPath, r.Role)
if roleBase == "" {
return errors.Errorf("cannot found Role %q", r.Role)
}
mainTask := getYamlFile(baseFS, filepath.Join(roleBase, _const.ProjectRolesTasksDir, _const.ProjectRolesTasksMainFile))
if mainTask == "" {
return errors.Errorf("cannot found main task for Role %q", r.Role)
}
rdata, err := fs.ReadFile(baseFS, mainTask)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", mainTask)
}
var blocks []kkprojectv1.Block
if err := yaml.Unmarshal(rdata, &blocks); err != nil {
return errors.Wrapf(err, "failed to unmarshal yaml file %q", filepath.Join(filepath.Dir(pbPath), mainTask))
}
p.Roles[i].Block = blocks
}
return nil
}
// convertRoles convert roleName to block
func convertRoles(baseFS fs.FS, pbPath string, pb *kkprojectv1.Playbook) error {
for i, p := range pb.Play {
for i, r := range p.Roles {
roleBase := getRoleBaseFromPlaybook(baseFS, pbPath, r.Role)
if roleBase == "" {
return errors.Errorf("cannot found Role %q in playbook %q", r.Role, pbPath)
}
var err error
if p.Roles[i].Block, err = convertRoleBlocks(baseFS, pbPath, roleBase); err != nil {
return err
}
if err = convertRoleVars(baseFS, roleBase, &p.Roles[i]); err != nil {
return err
}
}
pb.Play[i] = p
}
return nil
}
func convertRoleVars(baseFS fs.FS, roleBase string, role *kkprojectv1.Role) error {
// load defaults (optional)
defaultsFile := getYamlFile(baseFS, filepath.Join(roleBase, _const.ProjectRolesDefaultsDir, _const.ProjectRolesDefaultsMainFile))
if defaultsFile != "" {
data, err := fs.ReadFile(baseFS, defaultsFile)
if err != nil {
return errors.Wrapf(err, "failed to read defaults variable file %q", defaultsFile)
}
var newVars map[string]any
// Unmarshal the YAML document into a root node.
if err := yaml.Unmarshal(data, &newVars); err != nil {
return errors.Wrap(err, "failed to unmarshal YAML")
}
// store vars in play. the vars defined in file should not be repeated.
role.Vars = variable.CombineVariables(newVars, role.Vars)
}
return nil
}
// convertRoleBlocks roles/task/main.yaml to []kkprojectv1.Block
func convertRoleBlocks(baseFS fs.FS, pbPath string, roleBase string) ([]kkprojectv1.Block, error) {
mainTask := getYamlFile(baseFS, filepath.Join(roleBase, _const.ProjectRolesTasksDir, _const.ProjectRolesTasksMainFile))
if mainTask == "" {
return nil, errors.Errorf("cannot found main task for Role %q", roleBase)
}
rdata, err := fs.ReadFile(baseFS, mainTask)
if err != nil {
return nil, errors.Wrapf(err, "failed to read file %q", mainTask)
}
var blocks []kkprojectv1.Block
if err := yaml.Unmarshal(rdata, &blocks); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal yaml file %q", filepath.Join(filepath.Dir(pbPath), mainTask))
}
return blocks, nil
}
// convertIncludeTasks from file to blocks
func convertIncludeTasks(baseFS fs.FS, pbPath string, pb *kkprojectv1.Playbook) error {
var pbBase = filepath.Dir(filepath.Dir(pbPath))
for _, play := range pb.Play {
if err := fileToBlock(baseFS, pbBase, play.PreTasks); err != nil {
return err
}
if err := fileToBlock(baseFS, pbBase, play.Tasks); err != nil {
return err
}
if err := fileToBlock(baseFS, pbBase, play.PostTasks); err != nil {
return err
}
for _, r := range play.Roles {
roleBase := getRoleBaseFromPlaybook(baseFS, pbPath, r.Role)
if err := fileToBlock(baseFS, filepath.Join(roleBase, _const.ProjectRolesTasksDir), r.Block); err != nil {
return err
}
}
}
return nil
}
func fileToBlock(baseFS fs.FS, baseDir string, blocks []kkprojectv1.Block) error {
for i, b := range blocks {
if b.IncludeTasks != "" {
data, err := fs.ReadFile(baseFS, filepath.Join(baseDir, b.IncludeTasks))
if err != nil {
return errors.Wrapf(err, "failed to read includeTask file %q", filepath.Join(baseDir, b.IncludeTasks))
}
var bs []kkprojectv1.Block
if err := yaml.Unmarshal(data, &bs); err != nil {
return errors.Wrapf(err, "failed to unmarshal includeTask file %q", filepath.Join(baseDir, b.IncludeTasks))
}
b.Block = bs
blocks[i] = b
}
if err := fileToBlock(baseFS, baseDir, b.Block); err != nil {
return err
}
if err := fileToBlock(baseFS, baseDir, b.Rescue); err != nil {
return err
}
if err := fileToBlock(baseFS, baseDir, b.Always); err != nil {
return err
}
}
return nil
}
// getPlaybookBaseFromPlaybook find import_playbook path base on the current_playbook
// find from project/playbooks/playbook if exists.
// find from current_playbook/playbooks/playbook if exists.
// find current_playbook/playbook
func getPlaybookBaseFromPlaybook(baseFS fs.FS, pbPath string, playbook string) string {
var find []string
// find from project/playbooks/playbook
find = append(find, filepath.Join(filepath.Dir(filepath.Dir(pbPath)), _const.ProjectPlaybooksDir, playbook))
// find from pbPath dir like: current_playbook/playbooks/playbook
find = append(find, filepath.Join(filepath.Dir(pbPath), _const.ProjectPlaybooksDir, playbook))
// find from pbPath dir like: current_playbook/playbook
find = append(find, filepath.Join(filepath.Dir(pbPath), playbook))
for _, s := range find {
if baseFS != nil {
if _, err := fs.Stat(baseFS, s); err == nil {
return s
}
} else {
if _, err := os.Stat(s); err == nil {
return s
}
}
}
return ""
}
// getRoleBaseFromPlaybook
// find from project/roles/roleName if exists.
// find from current_playbook/roles/roleName if exists.
// find current_playbook/playbook
func getRoleBaseFromPlaybook(baseFS fs.FS, pbPath string, roleName string) string {
var find []string
// find from project/roles/roleName
find = append(find, filepath.Join(filepath.Dir(filepath.Dir(pbPath)), _const.ProjectRolesDir, roleName))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(filepath.Dir(pbPath), _const.ProjectRolesDir, roleName))
for _, s := range find {
if baseFS != nil {
if _, err := fs.Stat(baseFS, s); err == nil {
return s
}
} else {
if _, err := os.Stat(s); err == nil {
return s
}
}
}
return ""
}
// getYamlFile
// return *.yaml if exists
// return *.yml if exists.
func getYamlFile(baseFS fs.FS, base string) string {
var find []string
find = append(find, base+".yaml", base+".yml")
for _, s := range find {
if baseFS != nil {
if _, err := fs.Stat(baseFS, s); err == nil {
return s
}
} else {
if _, err := os.Stat(s); err == nil {
return s
}
}
}
return ""
}

View File

@ -1,235 +0,0 @@
/*
Copyright 2023 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 project
import (
"os"
"path/filepath"
"testing"
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
"github.com/stretchr/testify/assert"
)
func TestGetPlaybookBaseFromAbsPlaybook(t *testing.T) {
testcases := []struct {
name string
basePlaybook string
playbook string
except string
}{
{
name: "find from project/playbooks/playbook",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
playbook: "playbook2.yaml",
except: filepath.Join("playbooks", "playbook2.yaml"),
},
{
name: "find from current_playbook/playbooks/playbook",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
playbook: "playbook3.yaml",
except: filepath.Join("playbooks", "playbooks", "playbook3.yaml"),
},
{
name: "cannot find",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
playbook: "playbook4.yaml",
except: "",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.except, getPlaybookBaseFromPlaybook(os.DirFS("testdata"), tc.basePlaybook, tc.playbook))
})
}
}
func TestGetRoleBaseFromAbsPlaybook(t *testing.T) {
testcases := []struct {
name string
basePlaybook string
roleName string
except string
}{
{
name: "find from project/roles/roleName",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
roleName: "role1",
except: filepath.Join("roles", "role1"),
},
{
name: "find from current_playbook/roles/roleName",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
roleName: "role2",
except: filepath.Join("playbooks", "roles", "role2"),
},
{
name: "cannot find",
basePlaybook: filepath.Join("playbooks", "playbook1.yaml"),
roleName: "role3",
except: "",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.except, getRoleBaseFromPlaybook(os.DirFS("testdata"), tc.basePlaybook, tc.roleName))
})
}
}
func TestGetYamlFile(t *testing.T) {
testcases := []struct {
name string
base string
except string
}{
{
name: "get yaml",
base: filepath.Join("playbooks", "playbook2"),
except: filepath.Join("playbooks", "playbook2.yaml"),
},
{
name: "get yml",
base: filepath.Join("playbooks", "playbook3"),
except: filepath.Join("playbooks", "playbook3.yml"),
},
{
name: "cannot find",
base: filepath.Join("playbooks", "playbook4"),
except: "",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.except, getYamlFile(os.DirFS("testdata"), tc.base))
})
}
}
func TestMarshalPlaybook(t *testing.T) {
testcases := []struct {
name string
file string
except *kkprojectv1.Playbook
}{
{
name: "marshal playbook",
file: "playbooks/playbook1.yaml",
except: &kkprojectv1.Playbook{Play: []kkprojectv1.Play{
{
Base: kkprojectv1.Base{Name: "play1"},
PlayHost: kkprojectv1.PlayHost{Hosts: []string{"localhost"}},
Roles: []kkprojectv1.Role{
{
RoleInfo: kkprojectv1.RoleInfo{
Role: "role1",
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "role1 | block1"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
},
},
},
},
Handlers: nil,
PreTasks: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | pre_block1"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
},
PostTasks: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | post_block1"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
},
Tasks: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | block1"}},
BlockInfo: kkprojectv1.BlockInfo{Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | block1 | block1"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | block1 | block2"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
}},
},
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play1 | block2"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
},
},
{
Base: kkprojectv1.Base{Name: "play2"},
PlayHost: kkprojectv1.PlayHost{Hosts: []string{"localhost"}},
Tasks: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{Base: kkprojectv1.Base{Name: "play2 | block1"}},
Task: kkprojectv1.Task{UnknownField: map[string]any{
"debug": map[string]any{
"msg": "echo \"hello world\"",
},
}},
},
},
},
}},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
pb, err := marshalPlaybook(os.DirFS("testdata"), tc.file)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.except, pb)
})
}
}

View File

@ -17,15 +17,12 @@ limitations under the License.
package project
import (
"io/fs"
"os"
"path/filepath"
"github.com/cockroachdb/errors"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
)
func newLocalProject(playbook kkcorev1.Playbook) (Project, error) {
@ -43,84 +40,15 @@ func newLocalProject(playbook kkcorev1.Playbook) (Project, error) {
if _, err := os.Stat(playbook.Spec.Playbook); err != nil {
return nil, errors.Wrapf(err, "cannot find playbook %q", playbook.Spec.Playbook)
}
if filepath.Base(filepath.Dir(playbook.Spec.Playbook)) != _const.ProjectPlaybooksDir {
// the format of playbook is not correct
return nil, errors.New("playbook should be projectDir/playbooks/playbookfile")
}
projectDir := filepath.Dir(filepath.Dir(playbook.Spec.Playbook))
pb, err := filepath.Rel(projectDir, playbook.Spec.Playbook)
projectPath := GetProjectPath(playbook.Spec.Playbook)
relPath, err := filepath.Rel(projectPath, playbook.Spec.Playbook)
if err != nil {
return nil, errors.Wrapf(err, "failed to get rel path for playbook %q", playbook.Spec.Playbook)
return nil, errors.Wrapf(err, "failed to get relative path for playbook %q", playbook.Spec.Playbook)
}
return &localProject{Playbook: playbook, projectDir: projectDir, playbook: pb}, nil
}
type localProject struct {
kkcorev1.Playbook
projectDir string
// playbook relpath base on projectDir
playbook string
}
func (p localProject) getFilePath(path string, o GetFileOption) string {
if filepath.IsAbs(path) {
return path
}
var find []string
switch {
case o.IsFile:
if o.Role != "" {
// find from project/roles/roleName
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.projectDir, p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesFilesDir, path))
}
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesFilesDir, path))
case o.IsTemplate:
// find from project/roles/roleName
if o.Role != "" {
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
// find from pbPath dir like: current_playbook/roles/roleName
find = append(find, filepath.Join(p.projectDir, p.playbook, _const.ProjectRolesDir, o.Role, _const.ProjectRolesTemplateDir, path))
}
find = append(find, filepath.Join(p.projectDir, _const.ProjectRolesTemplateDir, path))
default:
find = append(find, filepath.Join(p.projectDir, path))
}
for _, s := range find {
if _, err := os.Stat(s); err == nil {
return s
}
}
return ""
}
// MarshalPlaybook project file to playbook.
func (p localProject) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
return marshalPlaybook(os.DirFS(p.projectDir), p.playbook)
}
// Stat role/file/template file or dir in project
func (p localProject) Stat(path string, option GetFileOption) (os.FileInfo, error) {
return os.Stat(p.getFilePath(path, option))
}
// WalkDir role/file/template dir in project
func (p localProject) WalkDir(path string, option GetFileOption, f fs.WalkDirFunc) error {
return filepath.WalkDir(p.getFilePath(path, option), f)
}
// ReadFile role/file/template file or dir in project
func (p localProject) ReadFile(path string, option GetFileOption) ([]byte, error) {
return os.ReadFile(p.getFilePath(path, option))
}
// Rel path for role/file/template file or dir in project
func (p localProject) Rel(root string, path string, option GetFileOption) (string, error) {
return filepath.Rel(p.getFilePath(root, option), path)
return &project{
FS: os.DirFS(projectPath),
basePlaybook: relPath,
Playbook: &kkprojectv1.Playbook{},
}, nil
}

187
pkg/project/path.go Normal file
View File

@ -0,0 +1,187 @@
package project
import (
"path/filepath"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
)
const (
// PathFormatImportPlaybook defines the directory structure for importing playbooks
// The structure supports three formats:
// 1. Import playbook in same directory as base playbook
// 2. Import playbook from playbooks/ subdirectory
// 3. Import playbook from project root playbooks/ directory
PathFormatImportPlaybook = `
|-- base-playbook.yaml
|-- [import_playbook]
|-- playbook.yaml
|-- playbooks/
| |-- [import_playbook]
|-- [projectDir]
|-- playbooks/
| |-- [import_playbook]
`
// PathFormatVarsFile defines the directory structure for vars files
// Vars files are expected to be in the same directory as the base playbook
PathFormatVarsFile = `
|-- base-playbook.yaml
|-- [VarsFile]
`
// PathFormatRole defines the directory structure for roles
// Roles can be found in three locations:
// 1. roles/ directory at project root
// 2. roles/ directory next to playbook
// 3. roles/ directory at project root
PathFormatRole = `
|-- playbooks/
| |-- playbook.yaml
|-- roles/
| |-- [role]/
|-- playbook.yaml
|-- roles/
| |-- [role]/
|-- [projectDir]
|-- roles/
| |-- [role]/
`
// PathFormatRoleTask defines the directory structure for role tasks
// Role tasks are expected to be in tasks/main.yaml or tasks/main.yml under the role directory
PathFormatRoleTask = `
|-- baseRole/
| |-- tasks/
| | |-- main.yaml
|-- baseRole/
| |-- tasks/
| | |-- main.yml
`
// PathFormatIncludeTask defines the directory structure for included tasks
// Tasks can be included from:
// 1. Direct path under source role
// 2. tasks/ directory under source role
// 3. Direct path under top level role
// 4. tasks/ directory under top level role
PathFormatIncludeTask = `
|-- [source_task]
| |-- tasks/
| | |-- [include_tasks]
|-- [source_task]
| |-- [include_tasks]
|-- [top_source_task]
| |-- tasks/
| | |-- [include_tasks]
|-- [top_source_task]
| |-- [include_tasks]
`
)
// GetProjectPath from basePlaybook. the Project directory structure can be either:
/*
|-- projectFS/
| |-- basePlaybook.yaml
|-- projectFS/
| |-- playbooks/
| | |-- basePlaybook.yaml
*/
func GetProjectPath(basePlaybook string) string {
if filepath.Base(filepath.Dir(basePlaybook)) == _const.ProjectPlaybooksDir {
return filepath.Dir(filepath.Dir(basePlaybook))
}
return filepath.Dir(basePlaybook)
}
// GetImportPlaybookRelPath returns possible relative paths for an imported playbook based on the base playbook location
// The format follows PathFormatImportPlaybook structure
func GetImportPlaybookRelPath(basePlaybook string, includePlaybook string) []string {
return []string{
filepath.Join(filepath.Dir(basePlaybook), includePlaybook),
filepath.Join(filepath.Dir(basePlaybook), _const.ProjectPlaybooksDir, includePlaybook),
// should support find playbooks from projectDir
filepath.Join(_const.ProjectPlaybooksDir, includePlaybook),
}
}
// GetVarsFilesRelPath returns possible relative paths for vars files based on the base playbook location
// The format follows PathFormatVarsFile structure
func GetVarsFilesRelPath(basePlaybook string, varsFile string) []string {
return []string{
filepath.Join(filepath.Dir(basePlaybook), varsFile),
}
}
// GetRoleRelPath returns possible relative paths for a role based on the base playbook location
// The format follows PathFormatRole structure
func GetRoleRelPath(basePlaybook string, role string) []string {
return []string{
filepath.Join(filepath.Dir(filepath.Dir(basePlaybook)), _const.ProjectRolesDir, role),
filepath.Join(filepath.Dir(basePlaybook), _const.ProjectRolesDir, role),
filepath.Join(_const.ProjectRolesDir, role),
}
}
// GetRoleTaskRelPath returns possible relative paths for a role's main task file
// The format follows PathFormatRoleTask structure
func GetRoleTaskRelPath(baseRole string) []string {
return []string{
filepath.Join(baseRole, _const.ProjectRolesTasksDir, "main.yaml"),
filepath.Join(baseRole, _const.ProjectRolesTasksDir, "main.yml"),
}
}
// GetRoleDefaultsRelPath returns possible relative paths for a role's defaults file
// The format follows similar structure to role tasks
func GetRoleDefaultsRelPath(baseRole string) []string {
return []string{
filepath.Join(baseRole, _const.ProjectRolesDefaultsDir, "main.yaml"),
filepath.Join(baseRole, _const.ProjectRolesDefaultsDir, "main.yml"),
}
}
// GetIncludeTaskRelPath returns possible relative paths for included task files
// The format follows PathFormatIncludeTask structure
func GetIncludeTaskRelPath(top string, source string, includeTask string) []string {
return []string{
filepath.Join(source, includeTask),
filepath.Join(source, _const.ProjectRolesTasksDir, includeTask),
filepath.Join(top, includeTask),
filepath.Join(top, _const.ProjectRolesTasksDir, includeTask),
}
}
// Standard project directory structure when playbook does not define a role and directly defines tasks:
/*
|-- projects_dir/
| |-- project1/
| | |-- playbook.yaml
| | |-- files/
| | |-- templates/
|-- projects_dir/
| |-- project1/
| | |-- playbooks/
| | | |-- playbook.yaml
| | | |-- files/
| | | |-- templates/
*/
// Standard roles directory structure:
/*
|-- roles/
| |-- defaults/
| |-- files/
| |-- tasks/
| |-- templates/
*/

View File

@ -14,18 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package project provides functionality for managing Ansible-like projects in KubeKey.
// It handles project file operations, playbook parsing, and task execution.
package project
import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/cockroachdb/errors"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
"gopkg.in/yaml.v3"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
// builtinProjectFunc is a function that creates a Project from a built-in playbook
var builtinProjectFunc func(kkcorev1.Playbook) (Project, error)
// Project represent location of actual project.
@ -34,26 +44,19 @@ type Project interface {
// MarshalPlaybook project file to playbook.
MarshalPlaybook() (*kkprojectv1.Playbook, error)
// Stat file or dir in project
Stat(path string, option GetFileOption) (os.FileInfo, error)
Stat(path string) (os.FileInfo, error)
// WalkDir dir in project
WalkDir(path string, option GetFileOption, f fs.WalkDirFunc) error
WalkDir(path string, f fs.WalkDirFunc) error
// ReadFile file or dir in project
ReadFile(path string, option GetFileOption) ([]byte, error)
ReadFile(path string) ([]byte, error)
// Rel path file or dir in project
Rel(root string, path string, option GetFileOption) (string, error)
Rel(root string, path string) (string, error)
}
// GetFileOption for file.
type GetFileOption struct {
Role string
IsTemplate bool
IsFile bool
}
// New project.
// If project address is git format. newGitProject
// If playbook has BuiltinsProjectAnnotation. builtinProjectFunc
// Default newLocalProject
// New creates a new Project instance based on the provided playbook.
// If project address is git format, it creates a git project.
// If playbook has BuiltinsProjectAnnotation, it uses builtinProjectFunc.
// Otherwise, it creates a local project.
func New(ctx context.Context, playbook kkcorev1.Playbook, update bool) (Project, error) {
if strings.HasPrefix(playbook.Spec.Project.Addr, "https://") ||
strings.HasPrefix(playbook.Spec.Project.Addr, "http://") ||
@ -67,3 +70,241 @@ func New(ctx context.Context, playbook kkcorev1.Playbook, update bool) (Project,
return newLocalProject(playbook)
}
// project implements the Project interface using an fs.FS
type project struct {
fs.FS
basePlaybook string
*kkprojectv1.Playbook
}
// ReadFile reads and returns the contents of the file at the given path
func (f *project) ReadFile(path string) ([]byte, error) {
return fs.ReadFile(f.FS, path)
}
// Rel returns a relative path that is lexically equivalent to targpath when joined to basepath
func (f *project) Rel(root string, path string) (string, error) {
return filepath.Rel(root, path)
}
// Stat returns the FileInfo for the file at the given path
func (f *project) Stat(path string) (os.FileInfo, error) {
return fs.Stat(f.FS, path)
}
// WalkDir walks the file tree rooted at path, calling fn for each file or directory
func (f *project) WalkDir(path string, fn fs.WalkDirFunc) error {
return fs.WalkDir(f.FS, path, fn)
}
// MarshalPlaybook converts a playbook file into a kkprojectv1.Playbook
func (f *project) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
f.Playbook = &kkprojectv1.Playbook{}
// convert playbook to kkprojectv1.Playbook
if err := f.loadPlaybook(f.basePlaybook); err != nil {
return nil, err
}
// convertIncludeTasks
if err := f.convertIncludeTasks(f.basePlaybook); err != nil {
return nil, err
}
// validate playbook
if err := f.Playbook.Validate(); err != nil {
return nil, err
}
return f.Playbook, nil
}
// loadPlaybook loads a playbook and all its included playbooks into a single playbook
func (f *project) loadPlaybook(basePlaybook string) error {
// baseDir is the local ansible project dir which playbook belong to
pbData, err := fs.ReadFile(f.FS, basePlaybook)
if err != nil {
return errors.Wrapf(err, "failed to read playbook %q", basePlaybook)
}
var plays []kkprojectv1.Play
if err := yaml.Unmarshal(pbData, &plays); err != nil {
return errors.Wrapf(err, "failed to unmarshal playbook %q", basePlaybook)
}
for _, p := range plays {
if err := f.dealImportPlaybook(p, basePlaybook); err != nil {
return err
}
if err := f.dealVarsFiles(&p, basePlaybook); err != nil {
return err
}
// fill block in roles
if err := f.dealRoles(p, basePlaybook); err != nil {
return err
}
f.Playbook.Play = append(f.Playbook.Play, p)
}
return nil
}
// dealImportPlaybook handles the "import_playbook" argument in a play
func (f *project) dealImportPlaybook(p kkprojectv1.Play, basePlaybook string) error {
if p.ImportPlaybook != "" {
importPlaybook := f.getPath(GetImportPlaybookRelPath(basePlaybook, p.ImportPlaybook))
if importPlaybook == "" {
return errors.Errorf("failed to find import_playbook %q base on %q. it's should be:\n %s", p.ImportPlaybook, basePlaybook, PathFormatImportPlaybook)
}
if err := f.loadPlaybook(importPlaybook); err != nil {
return err
}
}
return nil
}
// dealVarsFiles handles the "var_files" argument in a play
func (f *project) dealVarsFiles(p *kkprojectv1.Play, basePlaybook string) error {
for _, varsFile := range p.VarsFiles {
// load vars from vars_files
file := f.getPath(GetVarsFilesRelPath(basePlaybook, varsFile))
if file == "" {
return errors.Errorf("failed to find vars_files %q base on %q. it's should be:\n %s", varsFile, basePlaybook, PathFormatVarsFile)
}
data, err := fs.ReadFile(f.FS, file)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", file)
}
var newVars map[string]any
// Unmarshal the YAML document into a root node.
if err := yaml.Unmarshal(data, &newVars); err != nil {
return errors.Wrap(err, "failed to failed to unmarshal YAML")
}
// store vars in play. the vars defined in file should not be repeated.
p.Vars = variable.CombineVariables(newVars, p.Vars)
}
return nil
}
// dealRoles handles the "roles" argument in a play
func (f *project) dealRoles(p kkprojectv1.Play, basePlaybook string) error {
for i, r := range p.Roles {
baseRole := f.getPath(GetRoleRelPath(basePlaybook, r.Role))
if baseRole == "" {
return errors.Errorf("failed to find role %q base on %q. it's should be:\n %s", r.Role, basePlaybook, PathFormatRole)
}
// deal tasks
task := f.getPath(GetRoleTaskRelPath(baseRole))
if task == "" {
return errors.Errorf("cannot found main task for Role %q. it's should be: \n %s", r.Role, PathFormatRoleTask)
}
rdata, err := fs.ReadFile(f.FS, task)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", task)
}
var blocks []kkprojectv1.Block
if err := yaml.Unmarshal(rdata, &blocks); err != nil {
return errors.Wrapf(err, "failed to unmarshal yaml file %q", task)
}
p.Roles[i].Block = blocks
// deal defaults (optional)
if defaults := f.getPath(GetRoleDefaultsRelPath(baseRole)); defaults != "" {
data, err := fs.ReadFile(f.FS, defaults)
if err != nil {
return errors.Wrapf(err, "failed to read defaults variable file %q", defaults)
}
var newVars map[string]any
// Unmarshal the YAML document into a root node.
if err := yaml.Unmarshal(data, &newVars); err != nil {
return errors.Wrap(err, "failed to unmarshal YAML")
}
// store vars in play. the vars defined in file should not be repeated.
p.Roles[i].Vars = variable.CombineVariables(newVars, p.Roles[i].Vars)
}
}
return nil
}
// convertIncludeTasks converts tasks from files into blocks
func (f *project) convertIncludeTasks(basePlaybook string) error {
for _, play := range f.Playbook.Play {
if err := f.fileToBlock(filepath.Dir(basePlaybook), filepath.Dir(basePlaybook), play.PreTasks); err != nil {
return err
}
if err := f.fileToBlock(filepath.Dir(basePlaybook), filepath.Dir(basePlaybook), play.Tasks); err != nil {
return err
}
if err := f.fileToBlock(filepath.Dir(basePlaybook), filepath.Dir(basePlaybook), play.PostTasks); err != nil {
return err
}
for _, r := range play.Roles {
baseRole := f.getPath(GetRoleRelPath(basePlaybook, r.Role))
if baseRole == "" {
return errors.Errorf("failed to find role %q base on %q. it's should be:\n %s", r.Role, basePlaybook, PathFormatRole)
}
if err := f.fileToBlock(baseRole, filepath.Join(baseRole, _const.ProjectRolesTasksDir), r.Block); err != nil {
return err
}
}
}
return nil
}
// fileToBlock converts task files into blocks, handling include_tasks directives
func (f *project) fileToBlock(top string, source string, blocks []kkprojectv1.Block) error {
for i, block := range blocks {
switch {
case len(block.Block) != 0: // it blocks
if err := f.fileToBlock(top, source, block.Block); err != nil {
return err
}
if err := f.fileToBlock(top, source, block.Rescue); err != nil {
return err
}
if err := f.fileToBlock(top, source, block.Always); err != nil {
return err
}
case block.IncludeTasks != "": // it's include_tasks
includeTask := f.getPath(GetIncludeTaskRelPath(top, source, block.IncludeTasks))
if includeTask == "" {
return errors.Errorf("failed to find include_task %q base on %q. it's should be:\n %s", block.IncludeTasks, source, PathFormatIncludeTask)
}
data, err := fs.ReadFile(f.FS, includeTask)
if err != nil {
return errors.Wrapf(err, "failed to read includeTask file %q", includeTask)
}
var includeBlocks []kkprojectv1.Block
if err := yaml.Unmarshal(data, &includeBlocks); err != nil {
return errors.Wrapf(err, "failed to unmarshal includeTask file %q", includeTask)
}
if err := f.fileToBlock(top, filepath.Dir(includeTask), includeBlocks); err != nil {
return err
}
blocks[i].Block = includeBlocks
default: // it tasks
blocks[i].UnknownField["annotations"] = map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: top,
}
}
}
return nil
}
// getPath returns the first valid path from a list of possible paths
func (f *project) getPath(paths []string) string {
for _, path := range paths {
if _, err := fs.Stat(f.FS, path); err == nil {
return path
}
}
return ""
}

313
pkg/project/project_test.go Normal file
View File

@ -0,0 +1,313 @@
package project
import (
"testing"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
"github.com/stretchr/testify/assert"
)
func TestMarshalPlaybook(t *testing.T) {
testcases := []struct {
name string
playbook kkcorev1.Playbook
except *kkprojectv1.Playbook
}{
{
name: "test_playbook1",
playbook: kkcorev1.Playbook{
Spec: kkcorev1.PlaybookSpec{
Playbook: "testdata/playbook1.yaml",
},
},
except: &kkprojectv1.Playbook{
Play: []kkprojectv1.Play{
{
Base: kkprojectv1.Base{
Name: "playbook1",
},
PlayHost: kkprojectv1.PlayHost{
Hosts: []string{"node1"},
},
PreTasks: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task1",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: ".",
},
"debug": map[string]any{
"msg": "im task1",
},
},
},
},
},
},
},
},
},
{
name: "test_playbook2",
playbook: kkcorev1.Playbook{
Spec: kkcorev1.PlaybookSpec{
Playbook: "testdata/playbook2.yaml",
},
},
except: &kkprojectv1.Playbook{
Play: []kkprojectv1.Play{
{
Base: kkprojectv1.Base{
Name: "playbook2",
},
PlayHost: kkprojectv1.PlayHost{
Hosts: []string{"node1"},
},
Roles: []kkprojectv1.Role{
{
RoleInfo: kkprojectv1.RoleInfo{
Role: "role1",
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task1",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: "roles/role1",
},
"debug": map[string]any{
"msg": "im task1",
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "test_playbook3",
playbook: kkcorev1.Playbook{
Spec: kkcorev1.PlaybookSpec{
Playbook: "testdata/playbook3.yaml",
},
},
except: &kkprojectv1.Playbook{
Play: []kkprojectv1.Play{
{
Base: kkprojectv1.Base{
Name: "playbook3",
},
PlayHost: kkprojectv1.PlayHost{
Hosts: []string{"node1"},
},
Tasks: []kkprojectv1.Block{
{
IncludeTasks: "include_task1.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task1",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: ".",
},
"debug": map[string]any{
"msg": "im task1",
},
},
},
},
{
IncludeTasks: "include_task1_1.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task2",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: ".",
},
"debug": map[string]any{
"msg": "im task2",
},
},
},
},
},
},
},
{
IncludeTasks: "include_task1/include_task1_2.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task3",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: ".",
},
"debug": map[string]any{
"msg": "im task3",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "test_playbook4",
playbook: kkcorev1.Playbook{
Spec: kkcorev1.PlaybookSpec{
Playbook: "testdata/playbooks/playbook4.yaml",
},
},
except: &kkprojectv1.Playbook{
Play: []kkprojectv1.Play{
{
Base: kkprojectv1.Base{
Name: "playbook4_1",
},
PlayHost: kkprojectv1.PlayHost{
Hosts: []string{"node1"},
},
Roles: []kkprojectv1.Role{
{
RoleInfo: kkprojectv1.RoleInfo{
Role: "role2",
Block: []kkprojectv1.Block{
{
IncludeTasks: "include_task1/include_task1.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
IncludeTasks: "include_task2.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task2",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: "roles/role2",
},
"debug": map[string]any{
"msg": "im task2",
},
},
},
},
},
},
},
{
IncludeTasks: "include_task3.yaml",
BlockInfo: kkprojectv1.BlockInfo{
Block: []kkprojectv1.Block{
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task3",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: "roles/role2",
},
"debug": map[string]any{
"msg": "im task3",
},
},
},
},
},
},
},
{
BlockBase: kkprojectv1.BlockBase{
Base: kkprojectv1.Base{
Name: "task1",
},
},
Task: kkprojectv1.Task{
UnknownField: map[string]any{
"annotations": map[string]string{
kkcorev1alpha1.TaskAnnotationRelativePath: "roles/role2",
},
"debug": map[string]any{
"msg": "im task1",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
project, err := newLocalProject(tc.playbook)
if err != nil {
t.Fatal(err)
}
actual, err := project.MarshalPlaybook()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.except, actual)
})
}
}

8
pkg/project/testdata/playbook1.yaml vendored Normal file
View File

@ -0,0 +1,8 @@
---
- name: playbook1
hosts:
- node1
pre_tasks:
- name: task1
debug:
msg: "im task1"

6
pkg/project/testdata/playbook2.yaml vendored Normal file
View File

@ -0,0 +1,6 @@
---
- name: playbook2
hosts:
- node1
roles:
- role1

7
pkg/project/testdata/playbook3.yaml vendored Normal file
View File

@ -0,0 +1,7 @@
---
- name: playbook3
hosts:
- node1
tasks:
- include_tasks: include_task1.yaml

View File

@ -0,0 +1,6 @@
---
- name: playbook4_1
hosts:
- node1
roles:
- role2

View File

@ -1,30 +0,0 @@
- name: play1
hosts: localhost
pre_tasks:
- name: play1 | pre_block1
debug:
msg: echo "hello world"
tasks:
- name: play1 | block1
block:
- name: play1 | block1 | block1
debug:
msg: echo "hello world"
- name: play1 | block1 | block2
debug:
msg: echo "hello world"
- name: play1 | block2
debug:
msg: echo "hello world"
post_tasks:
- name: play1 | post_block1
debug:
msg: echo "hello world"
roles:
- role1
- name: play2
hosts: localhost
tasks:
- name: play2 | block1
debug:
msg: echo "hello world"

View File

@ -0,0 +1,2 @@
---
- import_playbook: import_playbook1/import_playbook1.yaml

View File

@ -1,3 +0,0 @@
- name: role1 | block1
debug:
msg: echo "hello world"

View File

@ -1,3 +1,4 @@
- name: role1 | block1
---
- name: task1
debug:
msg: echo "hello world"
msg: "im task1"

View File

@ -0,0 +1,8 @@
---
- include_tasks: include_task2.yaml
- include_tasks: include_task3.yaml
- name: task1
debug:
msg: "im task1"

View File

@ -0,0 +1,3 @@
- name: task2
debug:
msg: "im task2"

View File

@ -0,0 +1,3 @@
- name: task3
debug:
msg: "im task3"

View File

@ -0,0 +1,2 @@
---
- include_tasks: include_task1/include_task1.yaml

View File

@ -0,0 +1,8 @@
---
- name: task1
debug:
msg: "im task1"
- include_tasks: include_task1_1.yaml
- include_tasks: include_task1/include_task1_2.yaml

View File

@ -0,0 +1,4 @@
---
- name: task3
debug:
msg: "im task3"

View File

@ -0,0 +1,4 @@
---
- name: task2
debug:
msg: "im task2"

View File

@ -53,11 +53,12 @@ import (
// RestConfig replace the restconfig transport to proxy transport
func RestConfig(runtimedir string, restconfig *rest.Config) error {
restconfig.TLSClientConfig = rest.TLSClientConfig{}
transport, err := newProxyTransport(runtimedir, restconfig)
if err != nil {
return err
}
restconfig.TLSClientConfig = rest.TLSClientConfig{}
restconfig.Transport = transport
return nil