mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
4b257377ec
* Initially generated tmsUpload<...> files * First provisioning of parameters supported by tmsUpload step * Refer to Go step from tmsUpload.groovy * Initial client implementation * Reverting line delimiters in tmsUpoad.groovy back to Unix ones * Temporarily remove when-condition for Release stage * Define useGoStep parameter in tmsUpload.groovy * Unstash buildResult if useGoStep is true * No unstashing and empty credentials, when using go step * Register TmsUploadCommand in piper.go * Cleanup groovy-related changes - they will be temporarily implemented in a different repo * Make getting OAuth token success * Look through the code and cleanup it a bit * Read service key from Jenkins credentials store * Provide initial set of unit tests for methods in /pkg/tms/tms.go file * Minor improvements on logging response on http call error * Check, if positive HTTP status code is as expected * Cleanup tms.yaml file, provide additional unit test for tms.go * Provide unit test for the case, when request body contains spaces * Specify nodeExtDescriptorMapping parameter as of type map in tms.yaml * Implement client method for getting nodes * Write tests for GetNodes method * Add GetMtaExtDescriptor client method and cover it with unit tests * Provide first implementation for Update- and UploadMtaExtDescriptor client methods * Provide first implementation for Update- and UploadMtaExtDescriptor client methods * Provide UploadFile and UploadFileToNode client methods * Provide tests for Update- and UploadMtaExtDescriptor client methods * Write tests for FileUpload and FileUploadToNode client methods * Minor corrections * Remove some TODO comments * Rename some of response structures * Revert change for line delimiters in cmd/piper.go * Add uploadType string parameter to UploadFile and UploadRequest methods of uploader mock to reflect the changed Uploader implementation * Start to implement execution logic in tmsUpload.go * Changes in tms.yaml file - remove resources from inputs in tms.yaml and implement mtaPath parameter settings in the yaml file the same way, as it is done in cloudFoundryDeploy.yaml - rename tms.yaml to tmsUpload.yaml, since some generation policy changed meanwhile * Rename tms.yaml to tmsUpload.yaml and do go generate * Use provided proxy on communication with UAA and TMS * Set proxy even before getting OAuth token * Further implementation of tmsUpload.go * Continuation on implementing the tmsUpload.go executor * Get mtarFilePath and git commitId from commonPipelineEnvironment, if they are missing in configuration file + minor changes * Implement a happy path test for tmsUpload logic * Cover with unit tests main happy and error paths of tmsUpload.go logic * Extend set of unit tests for tmsUpload.go * Eliminate some TODOs, extend unit tests for tmsUpload.go * Delete some TODOs * Remove a couple of more TODOs from tms_test.go file * Provide additional unit test for error due unexpected positive http status code on upload * Revert back line delimiters in cmd/piper.go * Comment out file uploading calls in tmsUpload.go * Run go generate to update generated files * Convert line delimiters in tmsUpload.yaml to Unix ones, as well as provide new line character in the end of the file, plus minor fix for logging in tmsUpload.go file (pipeline complained) * Correct description of a parameter in tmsUpload.yaml, extend unit tests to check for trimming a slash in the end of TMS url for client methods that do upload * [minor] Add a comment in the test code * Add stashContent parameter to do unstashing in tmsUpload.groovy, remove some of the clarified TODOs * Uncomment uploading file calls in tmsUpload.go, declare buildResult stash in tmsUpload.yaml * Remove clarified TODOs from the tmsUpload.go file * Run go fmt for jenkins-library/pkg/tms * Do not get explicitly values from common pipeline environment - all configurations are provided in yaml file * Remove unused struct from tmsUpload_test.go * Run go fmt jenkins-library\pkg\tms * Revise descriptions of parameters provided in tmsUpload.yaml file * Specify STAGES scope for tmsUpload parameters * Provide STAGES scope for the tmsUpload parameters, provide default value for stashContent parameter * Remove trailing space from tmsUpload.yaml * Provide unit tests for proxy-related changes in http.go file * Improve proxy implementation in tmsUpload.go file * Make tmsServiceKey again a mandatory parameter * Run go generate command to make the generated files correspond the yaml state * Change line delimiters back to Unix ones (were switched while resolving the conflicts) * Remove trailing spaces from tmsUpload.yaml * Minor change in a comment to trigger pipelines with commit * Improve checks for zero-structs and for empty maps, as well as use different package to read files in the tests * Revert line endings in http.go * Revert comments formatting changes in files that do not belong to the tmsUpload step
415 lines
15 KiB
Go
415 lines
15 KiB
Go
package tms
|
|
|
|
import (
|
|
"bytes"
|
|
b64 "encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
piperHttp "github.com/SAP/jenkins-library/pkg/http"
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type AuthToken struct {
|
|
TokenType string `json:"token_type"`
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
|
|
type CommunicationInstance struct {
|
|
tmsUrl string
|
|
uaaUrl string
|
|
clientId string
|
|
clientSecret string
|
|
httpClient piperHttp.Uploader
|
|
logger *logrus.Entry
|
|
isVerbose bool
|
|
}
|
|
|
|
type Node struct {
|
|
Id int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type nodes struct {
|
|
Nodes []Node `json:"nodes"`
|
|
}
|
|
|
|
type MtaExtDescriptor struct {
|
|
Id int64 `json:"id"`
|
|
Description string `json:"description"`
|
|
MtaId string `json:"mtaId"`
|
|
MtaExtId string `json:"mtaExtId"`
|
|
MtaVersion string `json:"mtaVersion"`
|
|
LastChangedAt string `json:"lastChangedAt"`
|
|
}
|
|
|
|
type mtaExtDescriptors struct {
|
|
MtaExtDescriptors []MtaExtDescriptor `json:"mtaExtDescriptors"`
|
|
}
|
|
|
|
type FileInfo struct {
|
|
Id int64 `json:"fileId"`
|
|
Name string `json:"fileName"`
|
|
}
|
|
|
|
type NodeUploadResponseEntity struct {
|
|
TransportRequestId int64 `json:"transportRequestId"`
|
|
TransportRequestDescription string `json:"transportRequestDescription"`
|
|
QueueEntries []QueueEntry `json:"queueEntries"`
|
|
}
|
|
|
|
type QueueEntry struct {
|
|
Id int64 `json:"queueId"`
|
|
NodeId int64 `json:"nodeId"`
|
|
NodeName string `json:"nodeName"`
|
|
}
|
|
|
|
type NodeUploadRequestEntity struct {
|
|
ContentType string `json:"contentType"`
|
|
StorageType string `json:"storageType"`
|
|
NodeName string `json:"nodeName"`
|
|
Description string `json:"description"`
|
|
NamedUser string `json:"namedUser"`
|
|
Entries []Entry `json:"entries"`
|
|
}
|
|
|
|
type Entry struct {
|
|
Uri string `json:"uri"`
|
|
}
|
|
|
|
type CommunicationInterface interface {
|
|
GetNodes() ([]Node, error)
|
|
GetMtaExtDescriptor(nodeId int64, mtaId, mtaVersion string) (MtaExtDescriptor, error)
|
|
UpdateMtaExtDescriptor(nodeId, idOfMtaExtDescriptor int64, file, mtaVersion, description, namedUser string) (MtaExtDescriptor, error)
|
|
UploadMtaExtDescriptorToNode(nodeId int64, file, mtaVersion, description, namedUser string) (MtaExtDescriptor, error)
|
|
UploadFile(file, namedUser string) (FileInfo, error)
|
|
UploadFileToNode(nodeName, fileId, description, namedUser string) (NodeUploadResponseEntity, error)
|
|
}
|
|
|
|
// NewCommunicationInstance returns CommunicationInstance structure with http client prepared for communication with TMS backend
|
|
func NewCommunicationInstance(httpClient piperHttp.Uploader, tmsUrl, uaaUrl, clientId, clientSecret string, isVerbose bool) (*CommunicationInstance, error) {
|
|
logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/tms")
|
|
|
|
communicationInstance := &CommunicationInstance{
|
|
tmsUrl: tmsUrl,
|
|
uaaUrl: uaaUrl,
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
httpClient: httpClient,
|
|
logger: logger,
|
|
isVerbose: isVerbose,
|
|
}
|
|
|
|
token, err := communicationInstance.getOAuthToken()
|
|
if err != nil {
|
|
return communicationInstance, errors.Wrap(err, "Error fetching OAuth token")
|
|
}
|
|
log.RegisterSecret(token)
|
|
|
|
options := piperHttp.ClientOptions{
|
|
Token: token,
|
|
}
|
|
communicationInstance.httpClient.SetOptions(options)
|
|
|
|
return communicationInstance, nil
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) getOAuthToken() (string, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("OAuth token retrieval started")
|
|
communicationInstance.logger.Infof("uaaUrl: %v, clientId: %v", communicationInstance.uaaUrl, communicationInstance.clientId)
|
|
}
|
|
|
|
encodedUsernameColonPassword := b64.StdEncoding.EncodeToString([]byte(communicationInstance.clientId + ":" + communicationInstance.clientSecret))
|
|
header := http.Header{}
|
|
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
header.Add("Authorization", "Basic "+encodedUsernameColonPassword)
|
|
|
|
urlFormData := url.Values{
|
|
"username": {communicationInstance.clientId},
|
|
"password": {communicationInstance.clientSecret},
|
|
"grant_type": {"password"},
|
|
}
|
|
|
|
data, err := sendRequest(communicationInstance, http.MethodPost, "/oauth/token/?grant_type=client_credentials&response_type=token", strings.NewReader(urlFormData.Encode()), header, http.StatusOK, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var token AuthToken
|
|
json.Unmarshal(data, &token)
|
|
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("OAuth Token retrieved successfully")
|
|
}
|
|
return token.TokenType + " " + token.AccessToken, nil
|
|
}
|
|
|
|
func sendRequest(communicationInstance *CommunicationInstance, method, urlPathAndQuery string, body io.Reader, header http.Header, expectedStatusCode int, isTowardsUaa bool) ([]byte, error) {
|
|
var requestBody io.Reader
|
|
if body != nil {
|
|
closer := ioutil.NopCloser(body)
|
|
bodyBytes, _ := ioutil.ReadAll(closer)
|
|
requestBody = bytes.NewBuffer(bodyBytes)
|
|
defer closer.Close()
|
|
}
|
|
|
|
url := communicationInstance.tmsUrl
|
|
if isTowardsUaa {
|
|
url = communicationInstance.uaaUrl
|
|
}
|
|
url = strings.TrimSuffix(url, "/")
|
|
|
|
response, err := communicationInstance.httpClient.SendRequest(method, fmt.Sprintf("%v%v", url, urlPathAndQuery), requestBody, header, nil)
|
|
|
|
// err is not nil for HTTP status codes >= 300
|
|
if err != nil {
|
|
communicationInstance.logger.Errorf("HTTP request failed with error: %s", err)
|
|
communicationInstance.logResponseBody(response)
|
|
return nil, err
|
|
}
|
|
|
|
if response.StatusCode != expectedStatusCode {
|
|
return nil, fmt.Errorf("unexpected positive HTTP status code %v, while it was expected %v", response.StatusCode, expectedStatusCode)
|
|
}
|
|
|
|
data, _ := ioutil.ReadAll(response.Body)
|
|
if !isTowardsUaa && communicationInstance.isVerbose {
|
|
communicationInstance.logger.Debugf("Valid response body: %v", string(data))
|
|
}
|
|
defer response.Body.Close()
|
|
return data, nil
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) logResponseBody(response *http.Response) {
|
|
if response != nil && response.Body != nil {
|
|
data, _ := ioutil.ReadAll(response.Body)
|
|
communicationInstance.logger.Errorf("Response body: %s", data)
|
|
response.Body.Close()
|
|
}
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) GetNodes() ([]Node, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Obtaining nodes started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v", communicationInstance.tmsUrl)
|
|
}
|
|
|
|
header := http.Header{}
|
|
header.Add("Content-Type", "application/json")
|
|
|
|
var aNodes []Node
|
|
var data []byte
|
|
data, err := sendRequest(communicationInstance, http.MethodGet, "/v2/nodes", nil, header, http.StatusOK, false)
|
|
if err != nil {
|
|
return aNodes, err
|
|
}
|
|
|
|
var getNodesResponse nodes
|
|
json.Unmarshal(data, &getNodesResponse)
|
|
aNodes = getNodesResponse.Nodes
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Nodes obtained successfully")
|
|
}
|
|
return aNodes, nil
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) GetMtaExtDescriptor(nodeId int64, mtaId, mtaVersion string) (MtaExtDescriptor, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Get MTA extension descriptor started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v, nodeId: %v, mtaId: %v, mtaVersion: %v", communicationInstance.tmsUrl, nodeId, mtaId, mtaVersion)
|
|
}
|
|
|
|
header := http.Header{}
|
|
header.Add("Content-Type", "application/json")
|
|
|
|
var mtaExtDescriptor MtaExtDescriptor
|
|
var data []byte
|
|
data, err := sendRequest(communicationInstance, http.MethodGet, fmt.Sprintf("/v2/nodes/%v/mtaExtDescriptors?mtaId=%v&mtaVersion=%v", nodeId, mtaId, mtaVersion), nil, header, http.StatusOK, false)
|
|
if err != nil {
|
|
return mtaExtDescriptor, err
|
|
}
|
|
|
|
var getMtaExtDescriptorsResponse mtaExtDescriptors
|
|
json.Unmarshal(data, &getMtaExtDescriptorsResponse)
|
|
if len(getMtaExtDescriptorsResponse.MtaExtDescriptors) > 0 {
|
|
mtaExtDescriptor = getMtaExtDescriptorsResponse.MtaExtDescriptors[0]
|
|
}
|
|
|
|
if communicationInstance.isVerbose {
|
|
if mtaExtDescriptor != (MtaExtDescriptor{}) {
|
|
communicationInstance.logger.Info("MTA extension descriptor obtained successfully")
|
|
} else {
|
|
communicationInstance.logger.Warn("No MTA extension descriptor found")
|
|
}
|
|
}
|
|
return mtaExtDescriptor, nil
|
|
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) UploadFileToNode(nodeName, fileId, description, namedUser string) (NodeUploadResponseEntity, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Node upload started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v, nodeName: %v, fileId: %v, description: %v, namedUser: %v", communicationInstance.tmsUrl, nodeName, fileId, description, namedUser)
|
|
}
|
|
|
|
header := http.Header{}
|
|
header.Add("Content-Type", "application/json")
|
|
|
|
var nodeUploadResponseEntity NodeUploadResponseEntity
|
|
entry := Entry{Uri: fileId}
|
|
body := NodeUploadRequestEntity{ContentType: "MTA", StorageType: "FILE", NodeName: nodeName, Description: description, NamedUser: namedUser, Entries: []Entry{entry}}
|
|
bodyBytes, errMarshaling := json.Marshal(body)
|
|
if errMarshaling != nil {
|
|
return nodeUploadResponseEntity, errors.Wrapf(errMarshaling, "unable to marshal request body %v", body)
|
|
}
|
|
|
|
data, errSendRequest := sendRequest(communicationInstance, http.MethodPost, "/v2/nodes/upload", bytes.NewReader(bodyBytes), header, http.StatusOK, false)
|
|
if errSendRequest != nil {
|
|
return nodeUploadResponseEntity, errSendRequest
|
|
}
|
|
|
|
json.Unmarshal(data, &nodeUploadResponseEntity)
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Node upload executed successfully")
|
|
}
|
|
return nodeUploadResponseEntity, nil
|
|
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) UpdateMtaExtDescriptor(nodeId, idOfMtaExtDescriptor int64, file, mtaVersion, description, namedUser string) (MtaExtDescriptor, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Update of MTA extension descriptor started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v, nodeId: %v, mtaExtDescriptorId: %v, file: %v, mtaVersion: %v, description: %v, namedUser: %v", communicationInstance.tmsUrl, nodeId, idOfMtaExtDescriptor, file, mtaVersion, description, namedUser)
|
|
}
|
|
|
|
header := http.Header{}
|
|
header.Add("tms-named-user", namedUser)
|
|
|
|
tmsUrl := strings.TrimSuffix(communicationInstance.tmsUrl, "/")
|
|
url := fmt.Sprintf("%v/v2/nodes/%v/mtaExtDescriptors/%v", tmsUrl, nodeId, idOfMtaExtDescriptor)
|
|
formFields := map[string]string{"mtaVersion": mtaVersion, "description": description}
|
|
|
|
var mtaExtDescriptor MtaExtDescriptor
|
|
fileHandle, errOpenFile := os.Open(file)
|
|
if errOpenFile != nil {
|
|
return mtaExtDescriptor, errors.Wrapf(errOpenFile, "unable to locate file %v", file)
|
|
}
|
|
defer fileHandle.Close()
|
|
|
|
uploadRequestData := piperHttp.UploadRequestData{Method: http.MethodPut, URL: url, File: file, FileFieldName: "file", FormFields: formFields, FileContent: fileHandle, Header: header, Cookies: nil}
|
|
|
|
var data []byte
|
|
data, errUpload := upload(communicationInstance, uploadRequestData, http.StatusOK)
|
|
if errUpload != nil {
|
|
return mtaExtDescriptor, errUpload
|
|
}
|
|
|
|
json.Unmarshal(data, &mtaExtDescriptor)
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("MTA extension descriptor updated successfully")
|
|
}
|
|
return mtaExtDescriptor, nil
|
|
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) UploadMtaExtDescriptorToNode(nodeId int64, file, mtaVersion, description, namedUser string) (MtaExtDescriptor, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Upload of MTA extension descriptor started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v, nodeId: %v, file: %v, mtaVersion: %v, description: %v, namedUser: %v", communicationInstance.tmsUrl, nodeId, file, mtaVersion, description, namedUser)
|
|
}
|
|
|
|
header := http.Header{}
|
|
header.Add("tms-named-user", namedUser)
|
|
|
|
tmsUrl := strings.TrimSuffix(communicationInstance.tmsUrl, "/")
|
|
url := fmt.Sprintf("%v/v2/nodes/%v/mtaExtDescriptors", tmsUrl, nodeId)
|
|
formFields := map[string]string{"mtaVersion": mtaVersion, "description": description}
|
|
|
|
var mtaExtDescriptor MtaExtDescriptor
|
|
fileHandle, errOpenFile := os.Open(file)
|
|
if errOpenFile != nil {
|
|
return mtaExtDescriptor, errors.Wrapf(errOpenFile, "unable to locate file %v", file)
|
|
}
|
|
defer fileHandle.Close()
|
|
|
|
uploadRequestData := piperHttp.UploadRequestData{Method: http.MethodPost, URL: url, File: file, FileFieldName: "file", FormFields: formFields, FileContent: fileHandle, Header: header, Cookies: nil}
|
|
|
|
var data []byte
|
|
data, errUpload := upload(communicationInstance, uploadRequestData, http.StatusCreated)
|
|
if errUpload != nil {
|
|
return mtaExtDescriptor, errUpload
|
|
}
|
|
|
|
json.Unmarshal(data, &mtaExtDescriptor)
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("MTA extension descriptor uploaded successfully")
|
|
}
|
|
return mtaExtDescriptor, nil
|
|
|
|
}
|
|
|
|
func (communicationInstance *CommunicationInstance) UploadFile(file, namedUser string) (FileInfo, error) {
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("Upload of file started")
|
|
communicationInstance.logger.Infof("tmsUrl: %v, file: %v, namedUser: %v", communicationInstance.tmsUrl, file, namedUser)
|
|
}
|
|
|
|
tmsUrl := strings.TrimSuffix(communicationInstance.tmsUrl, "/")
|
|
url := fmt.Sprintf("%v/v2/files/upload", tmsUrl)
|
|
formFields := map[string]string{"namedUser": namedUser}
|
|
|
|
var fileInfo FileInfo
|
|
fileHandle, errOpenFile := os.Open(file)
|
|
if errOpenFile != nil {
|
|
return fileInfo, errors.Wrapf(errOpenFile, "unable to locate file %v", file)
|
|
}
|
|
defer fileHandle.Close()
|
|
|
|
uploadRequestData := piperHttp.UploadRequestData{Method: http.MethodPost, URL: url, File: file, FileFieldName: "file", FormFields: formFields, FileContent: fileHandle, Header: http.Header{}, Cookies: nil}
|
|
|
|
var data []byte
|
|
data, errUpload := upload(communicationInstance, uploadRequestData, http.StatusCreated)
|
|
if errUpload != nil {
|
|
return fileInfo, errUpload
|
|
}
|
|
|
|
json.Unmarshal(data, &fileInfo)
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Info("File uploaded successfully")
|
|
}
|
|
return fileInfo, nil
|
|
|
|
}
|
|
|
|
func upload(communicationInstance *CommunicationInstance, uploadRequestData piperHttp.UploadRequestData, expectedStatusCode int) ([]byte, error) {
|
|
response, err := communicationInstance.httpClient.Upload(uploadRequestData)
|
|
|
|
// err is not nil for HTTP status codes >= 300
|
|
if err != nil {
|
|
communicationInstance.logger.Errorf("HTTP request failed with error: %s", err)
|
|
communicationInstance.logResponseBody(response)
|
|
return nil, err
|
|
}
|
|
|
|
if response.StatusCode != expectedStatusCode {
|
|
return nil, fmt.Errorf("unexpected positive HTTP status code %v, while it was expected %v", response.StatusCode, expectedStatusCode)
|
|
}
|
|
|
|
data, _ := ioutil.ReadAll(response.Body)
|
|
if communicationInstance.isVerbose {
|
|
communicationInstance.logger.Debugf("Valid response body: %v", string(data))
|
|
}
|
|
defer response.Body.Close()
|
|
return data, nil
|
|
}
|