github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/testsource.go (about)

     1  package v1
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/gofiber/fiber/v2"
     9  	"k8s.io/apimachinery/pkg/api/errors"
    10  	"k8s.io/apimachinery/pkg/util/yaml"
    11  
    12  	testsourcev1 "github.com/kubeshop/testkube-operator/api/testsource/v1"
    13  	"github.com/kubeshop/testkube-operator/pkg/client/testsources/v1"
    14  	"github.com/kubeshop/testkube-operator/pkg/secret"
    15  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    16  	"github.com/kubeshop/testkube/pkg/crd"
    17  	"github.com/kubeshop/testkube/pkg/executor/client"
    18  	testsourcesmapper "github.com/kubeshop/testkube/pkg/mapper/testsources"
    19  )
    20  
    21  func (s TestkubeAPI) CreateTestSourceHandler() fiber.Handler {
    22  	return func(c *fiber.Ctx) error {
    23  		errPrefix := "failed to create test source"
    24  		var testSource testsourcev1.TestSource
    25  		var secrets map[string]string
    26  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
    27  			testSourceSpec := string(c.Body())
    28  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSourceSpec), len(testSourceSpec))
    29  			if err := decoder.Decode(&testSource); err != nil {
    30  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
    31  			}
    32  		} else {
    33  			var request testkube.TestSourceUpsertRequest
    34  			err := c.BodyParser(&request)
    35  			if err != nil {
    36  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json request: %s", errPrefix, err))
    37  			}
    38  
    39  			if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
    40  				if request.Data != "" {
    41  					request.Data = fmt.Sprintf("%q", request.Data)
    42  				}
    43  
    44  				data, err := crd.GenerateYAML(crd.TemplateTestSource, []testkube.TestSourceUpsertRequest{request})
    45  				return s.getCRDs(c, data, err)
    46  			}
    47  
    48  			testSource = testsourcesmapper.MapAPIToCRD(request)
    49  			testSource.Namespace = s.Namespace
    50  			if request.Repository != nil && !s.disableSecretCreation {
    51  				secrets = createTestSecretsData(request.Repository.Username, request.Repository.Token)
    52  			}
    53  		}
    54  
    55  		created, err := s.TestSourcesClient.Create(&testSource, testsources.Option{Secrets: secrets})
    56  		if err != nil {
    57  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create test source: %w", errPrefix, err))
    58  		}
    59  
    60  		c.Status(http.StatusCreated)
    61  		return c.JSON(created)
    62  	}
    63  }
    64  
    65  func (s TestkubeAPI) UpdateTestSourceHandler() fiber.Handler {
    66  	return func(c *fiber.Ctx) error {
    67  		errPrefix := "failed to update test source"
    68  		var request testkube.TestSourceUpdateRequest
    69  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
    70  			var testSource testsourcev1.TestSource
    71  			testSourceSpec := string(c.Body())
    72  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSourceSpec), len(testSourceSpec))
    73  			if err := decoder.Decode(&testSource); err != nil {
    74  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
    75  			}
    76  
    77  			request = testsourcesmapper.MapSpecToUpdate(&testSource)
    78  		} else {
    79  			err := c.BodyParser(&request)
    80  			if err != nil {
    81  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json jrequest: %s", errPrefix, err))
    82  			}
    83  		}
    84  
    85  		var name string
    86  		if request.Name != nil {
    87  			name = *request.Name
    88  		}
    89  		errPrefix = errPrefix + " " + name
    90  		// we need to get resource first and load its metadata.ResourceVersion
    91  		testSource, err := s.TestSourcesClient.Get(name)
    92  		if err != nil {
    93  			if errors.IsNotFound(err) {
    94  				return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test source not found: %w", errPrefix, err))
    95  			}
    96  
    97  			return s.Error(c, http.StatusBadGateway, err)
    98  		}
    99  
   100  		// map update test source but load spec only to not override metadata.ResourceVersion
   101  		testSourceSpec := testsourcesmapper.MapUpdateToSpec(request, testSource)
   102  
   103  		var option *testsources.Option
   104  		if request.Repository != nil && (*request.Repository) != nil {
   105  			username := (*request.Repository).Username
   106  			token := (*request.Repository).Token
   107  			if (username != nil || token != nil) && !s.disableSecretCreation {
   108  				data, err := s.SecretClient.Get(secret.GetMetadataName(name, client.SecretSource))
   109  				if err != nil && !errors.IsNotFound(err) {
   110  					return s.Error(c, http.StatusBadGateway, err)
   111  				}
   112  
   113  				option = &testsources.Option{Secrets: updateTestSecretsData(data, username, token)}
   114  			}
   115  		}
   116  
   117  		var updatedTestSource *testsourcev1.TestSource
   118  		if option != nil {
   119  			updatedTestSource, err = s.TestSourcesClient.Update(testSourceSpec, *option)
   120  		} else {
   121  			updatedTestSource, err = s.TestSourcesClient.Update(testSourceSpec)
   122  		}
   123  
   124  		if err != nil {
   125  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client coult not update test source: %w", errPrefix, err))
   126  		}
   127  
   128  		return c.JSON(updatedTestSource)
   129  	}
   130  }
   131  
   132  func (s TestkubeAPI) ListTestSourcesHandler() fiber.Handler {
   133  	return func(c *fiber.Ctx) error {
   134  		errPrefix := "failed to list test sources"
   135  
   136  		list, err := s.TestSourcesClient.List(c.Query("selector"))
   137  		if err != nil {
   138  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test sources: %s", errPrefix, err))
   139  		}
   140  
   141  		results := []testkube.TestSource{}
   142  		for _, item := range list.Items {
   143  			results = append(results, testsourcesmapper.MapCRDToAPI(item))
   144  		}
   145  
   146  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   147  			for i := range results {
   148  				if results[i].Data != "" {
   149  					results[i].Data = fmt.Sprintf("%q", results[i].Data)
   150  				}
   151  			}
   152  
   153  			data, err := crd.GenerateYAML(crd.TemplateTestSource, results)
   154  			return s.getCRDs(c, data, err)
   155  		}
   156  
   157  		return c.JSON(results)
   158  	}
   159  }
   160  
   161  func (s TestkubeAPI) GetTestSourceHandler() fiber.Handler {
   162  	return func(c *fiber.Ctx) error {
   163  		name := c.Params("name")
   164  		errPrefix := "failed to get test source" + name
   165  
   166  		item, err := s.TestSourcesClient.Get(name)
   167  		if err != nil {
   168  			if errors.IsNotFound(err) {
   169  				return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: client could not find test source: %w", errPrefix, err))
   170  			}
   171  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test source: %w", errPrefix, err))
   172  		}
   173  
   174  		result := testsourcesmapper.MapCRDToAPI(*item)
   175  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   176  			if result.Data != "" {
   177  				result.Data = fmt.Sprintf("%q", result.Data)
   178  			}
   179  
   180  			data, err := crd.GenerateYAML(crd.TemplateTestSource, []testkube.TestSource{result})
   181  			return s.getCRDs(c, data, err)
   182  		}
   183  
   184  		return c.JSON(result)
   185  	}
   186  }
   187  
   188  func (s TestkubeAPI) DeleteTestSourceHandler() fiber.Handler {
   189  	return func(c *fiber.Ctx) error {
   190  		name := c.Params("name")
   191  		errPrefix := "failed to delete test source" + name
   192  
   193  		err := s.TestSourcesClient.Delete(name)
   194  		if err != nil {
   195  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test source: %w", errPrefix, err))
   196  		}
   197  
   198  		c.Status(http.StatusNoContent)
   199  		return nil
   200  	}
   201  }
   202  
   203  func (s TestkubeAPI) DeleteTestSourcesHandler() fiber.Handler {
   204  	return func(c *fiber.Ctx) error {
   205  		errPrefix := "failed to delete test sources"
   206  		err := s.TestSourcesClient.DeleteByLabels(c.Query("selector"))
   207  		if err != nil {
   208  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test sources: %w", errPrefix, err))
   209  		}
   210  
   211  		c.Status(http.StatusNoContent)
   212  		return nil
   213  	}
   214  }
   215  
   216  func (s TestkubeAPI) ProcessTestSourceBatchHandler() fiber.Handler {
   217  	return func(c *fiber.Ctx) error {
   218  		errPrefix := "failed to batch process test sources"
   219  
   220  		var request testkube.TestSourceBatchRequest
   221  		err := c.BodyParser(&request)
   222  		if err != nil {
   223  			return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse request: %s", errPrefix, err))
   224  		}
   225  
   226  		testSourceBatch := make(map[string]testkube.TestSourceUpsertRequest, len(request.Batch))
   227  		for _, item := range request.Batch {
   228  			if _, ok := testSourceBatch[item.Name]; ok {
   229  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test source with duplicated id/name %s", errPrefix, item.Name))
   230  			}
   231  
   232  			testSourceBatch[item.Name] = item
   233  		}
   234  
   235  		list, err := s.TestSourcesClient.List("")
   236  		if err != nil {
   237  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test sources: %w", errPrefix, err))
   238  		}
   239  
   240  		testSourceMap := make(map[string]testsourcev1.TestSource, len(list.Items))
   241  		for _, item := range list.Items {
   242  			testSourceMap[item.Name] = item
   243  		}
   244  
   245  		var result testkube.TestSourceBatchResult
   246  		for name, item := range testSourceBatch {
   247  			testSource := testsourcesmapper.MapAPIToCRD(item)
   248  			var username, token string
   249  			if item.Repository != nil && !s.disableSecretCreation {
   250  				username = item.Repository.Username
   251  				token = item.Repository.Token
   252  			}
   253  
   254  			if existed, ok := testSourceMap[name]; !ok {
   255  				testSource.Namespace = s.Namespace
   256  
   257  				created, err := s.TestSourcesClient.Create(&testSource, testsources.Option{Secrets: getTestSourceSecretsData(username, token)})
   258  				if err != nil {
   259  					return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create test source %s: %w", errPrefix, testSource.Name, err))
   260  				}
   261  
   262  				result.Created = append(result.Created, created.Name)
   263  			} else {
   264  				existed.Spec = testSource.Spec
   265  				existed.Labels = item.Labels
   266  
   267  				updated, err := s.TestSourcesClient.Update(&existed, testsources.Option{Secrets: getTestSourceSecretsData(username, token)})
   268  				if err != nil {
   269  					return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not update test source %s: %w", errPrefix, testSource.Name, err))
   270  				}
   271  
   272  				result.Updated = append(result.Updated, updated.Name)
   273  			}
   274  		}
   275  
   276  		for name := range testSourceMap {
   277  			if _, ok := testSourceBatch[name]; !ok {
   278  				err := s.TestSourcesClient.Delete(name)
   279  				if err != nil {
   280  					return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test source %s: %w", errPrefix, name, err))
   281  				}
   282  
   283  				result.Deleted = append(result.Deleted, name)
   284  			}
   285  		}
   286  
   287  		return c.JSON(result)
   288  	}
   289  }
   290  
   291  func getTestSourceSecretsData(username, token string) map[string]string {
   292  	if username == "" && token == "" {
   293  		return nil
   294  	}
   295  
   296  	data := make(map[string]string, 0)
   297  	if username != "" {
   298  		data[client.GitUsernameSecretName] = username
   299  	}
   300  
   301  	if token != "" {
   302  		data[client.GitTokenSecretName] = token
   303  	}
   304  
   305  	return data
   306  }