open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/automation/ansible.go (about)

     1  // Copyright Contributors to the Open Cluster Management project
     2  
     3  package automation
     4  
     5  import (
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"reflect"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  	"k8s.io/apimachinery/pkg/runtime/schema"
    17  	"k8s.io/client-go/dynamic"
    18  
    19  	policyv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1"
    20  )
    21  
    22  const (
    23  	PolicyAutomationLabel      string = "policy.open-cluster-management.io/policyautomation-name"
    24  	PolicyAutomationGeneration string = "policy.open-cluster-management.io/policyautomation-generation"
    25  	// policyautomation-ResouceVersion
    26  	PolicyAutomationResouceV string = "policy.open-cluster-management.io/policyautomation-resource-version"
    27  )
    28  
    29  var ansibleJobRes = schema.GroupVersionResource{
    30  	Group: "tower.ansible.com", Version: "v1alpha1",
    31  	Resource: "ansiblejobs",
    32  }
    33  
    34  // Check any ansiblejob is made by input genteration number
    35  // Returning "true" means the policy automation already created ansiblejob with the generation
    36  func MatchPAGeneration(policyAutomation *policyv1beta1.PolicyAutomation,
    37  	dynamicClient dynamic.Interface, generation int64,
    38  ) (bool, error) {
    39  	ansiblejobList, err := dynamicClient.Resource(ansibleJobRes).Namespace(policyAutomation.GetNamespace()).List(
    40  		context.TODO(), metav1.ListOptions{
    41  			LabelSelector: fmt.Sprintf("%s=%s", PolicyAutomationLabel, policyAutomation.GetName()),
    42  		},
    43  	)
    44  	if err != nil {
    45  		log.Error(err, "Failed to get ansiblejob list")
    46  
    47  		return false, err
    48  	}
    49  
    50  	s := strconv.FormatInt(generation, 10)
    51  
    52  	for _, aj := range ansiblejobList.Items {
    53  		annotations := aj.GetAnnotations()
    54  		if annotations[PolicyAutomationGeneration] == s {
    55  			return true, nil
    56  		}
    57  	}
    58  
    59  	return false, nil
    60  }
    61  
    62  // Check any ansiblejob is made by current resourceVersion number
    63  // Returning "true" means the policy automation already created ansiblejob with this resourceVersion
    64  func MatchPAResouceV(policyAutomation *policyv1beta1.PolicyAutomation,
    65  	dynamicClient dynamic.Interface, resourceVersion string,
    66  ) (bool, error) {
    67  	ansiblejobList, err := dynamicClient.Resource(ansibleJobRes).Namespace(policyAutomation.GetNamespace()).List(
    68  		context.TODO(), metav1.ListOptions{
    69  			LabelSelector: fmt.Sprintf("%s=%s", PolicyAutomationLabel, policyAutomation.GetName()),
    70  		},
    71  	)
    72  	if err != nil {
    73  		log.Error(err, "Failed to get ansiblejob list")
    74  
    75  		return false, err
    76  	}
    77  
    78  	for _, aj := range ansiblejobList.Items {
    79  		annotations := aj.GetAnnotations()
    80  		if annotations[PolicyAutomationResouceV] == resourceVersion {
    81  			return true, nil
    82  		}
    83  	}
    84  
    85  	return false, nil
    86  }
    87  
    88  // CreateAnsibleJob creates ansiblejob with given PolicyAutomation
    89  func CreateAnsibleJob(policyAutomation *policyv1beta1.PolicyAutomation,
    90  	dynamicClient dynamic.Interface, mode string, violationContext policyv1beta1.ViolationContext,
    91  ) error {
    92  	ansibleJob := &unstructured.Unstructured{
    93  		Object: map[string]interface{}{
    94  			"apiVersion": "tower.ansible.com/v1alpha1",
    95  			"kind":       "AnsibleJob",
    96  			"metadata": map[string]interface{}{
    97  				"annotations": map[string]interface{}{
    98  					PolicyAutomationGeneration: strconv.
    99  						FormatInt(policyAutomation.GetGeneration(), 10),
   100  					PolicyAutomationResouceV: policyAutomation.GetResourceVersion(),
   101  				},
   102  			},
   103  			"spec": map[string]interface{}{
   104  				"job_template_name": policyAutomation.Spec.Automation.Name,
   105  				"tower_auth_secret": policyAutomation.Spec.Automation.TowerSecret,
   106  				"extra_vars":        map[string]interface{}{},
   107  				"job_ttl":           86400, // default TTL is 24 hours
   108  			},
   109  		},
   110  	}
   111  
   112  	mapExtraVars := map[string]interface{}{}
   113  	if policyAutomation.Spec.Automation.ExtraVars != nil {
   114  		// This is to translate the runtime.RawExtension to a map[string]interface{}
   115  		err := json.Unmarshal(policyAutomation.Spec.Automation.ExtraVars.Raw, &mapExtraVars)
   116  		if err != nil {
   117  			return err
   118  		}
   119  	}
   120  
   121  	values := reflect.ValueOf(violationContext)
   122  	typesOf := values.Type()
   123  	// add every violationContext fields into mapExtraVars as well as the empty values,
   124  	// or when the whole violationContext is empty
   125  	for i := 0; i < values.NumField(); i++ {
   126  		tag := typesOf.Field(i).Tag
   127  		value := values.Field(i).Interface()
   128  
   129  		var fieldName string
   130  		if tag.Get("ansibleJob") != "" {
   131  			fieldName = tag.Get("ansibleJob")
   132  		} else if tag.Get("json") != "" {
   133  			fieldName = strings.SplitN(tag.Get("json"), ",", 2)[0]
   134  		} else {
   135  			fieldName = typesOf.Field(i).Name
   136  		}
   137  
   138  		mapExtraVars[fieldName] = value
   139  	}
   140  
   141  	label := map[string]string{
   142  		PolicyAutomationLabel: policyAutomation.GetName(),
   143  	}
   144  	ansibleJob.SetLabels(label)
   145  
   146  	ansibleJob.Object["spec"].(map[string]interface{})["extra_vars"] = mapExtraVars
   147  
   148  	if policyAutomation.Spec.Automation.JobTTL != nil {
   149  		ansibleJob.Object["spec"].(map[string]interface{})["job_ttl"] = *policyAutomation.Spec.Automation.JobTTL
   150  	}
   151  
   152  	ansibleJobRes := schema.GroupVersionResource{
   153  		Group: "tower.ansible.com", Version: "v1alpha1",
   154  		Resource: "ansiblejobs",
   155  	}
   156  
   157  	// Replace all special characters with hyphens. The ansible tower API requires this.
   158  	nonAlphaNumerics := regexp.MustCompile("[^a-zA-Z0-9]")
   159  	cleanName := nonAlphaNumerics.ReplaceAllString(policyAutomation.GetName(), "-")
   160  
   161  	// Ansible tower API requires the lower case naming
   162  	ansibleJob.SetGenerateName(strings.ToLower(cleanName + "-" + mode + "-"))
   163  	ansibleJob.SetOwnerReferences([]metav1.OwnerReference{
   164  		*metav1.NewControllerRef(policyAutomation, policyAutomation.GroupVersionKind()),
   165  	})
   166  
   167  	_, err := dynamicClient.Resource(ansibleJobRes).Namespace(policyAutomation.GetNamespace()).
   168  		Create(context.TODO(), ansibleJob, metav1.CreateOptions{})
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	return nil
   174  }