github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/git/gitea.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package git contains functions for interacting with git repositories.
     5  package git
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"time"
    14  
    15  	netHttp "net/http"
    16  
    17  	"github.com/Racer159/jackal/src/config"
    18  	"github.com/Racer159/jackal/src/pkg/cluster"
    19  	"github.com/Racer159/jackal/src/pkg/k8s"
    20  	"github.com/Racer159/jackal/src/pkg/message"
    21  	"github.com/Racer159/jackal/src/types"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  )
    24  
    25  // CreateTokenResponse is the response given from creating a token in Gitea
    26  type CreateTokenResponse struct {
    27  	ID             int64  `json:"id"`
    28  	Name           string `json:"name"`
    29  	Sha1           string `json:"sha1"`
    30  	TokenLastEight string `json:"token_last_eight"`
    31  }
    32  
    33  // CreateReadOnlyUser uses the Gitea API to create a non-admin Jackal user.
    34  func (g *Git) CreateReadOnlyUser() error {
    35  	message.Debugf("git.CreateReadOnlyUser()")
    36  
    37  	c, err := cluster.NewCluster()
    38  	if err != nil {
    39  		return err
    40  	}
    41  
    42  	// Establish a git tunnel to send the repo
    43  	tunnel, err := c.NewTunnel(cluster.JackalNamespaceName, k8s.SvcResource, cluster.JackalGitServerName, "", 0, cluster.JackalGitServerPort)
    44  	if err != nil {
    45  		return err
    46  	}
    47  	_, err = tunnel.Connect()
    48  	if err != nil {
    49  		return err
    50  	}
    51  	defer tunnel.Close()
    52  
    53  	tunnelURL := tunnel.HTTPEndpoint()
    54  
    55  	// Create json representation of the create-user request body
    56  	createUserBody := map[string]interface{}{
    57  		"username":             g.Server.PullUsername,
    58  		"password":             g.Server.PullPassword,
    59  		"email":                "jackal-reader@localhost.local",
    60  		"must_change_password": false,
    61  	}
    62  	createUserData, err := json.Marshal(createUserBody)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	var out []byte
    68  	var statusCode int
    69  
    70  	// Send API request to create the user
    71  	createUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users", tunnelURL)
    72  	createUserRequest, _ := netHttp.NewRequest("POST", createUserEndpoint, bytes.NewBuffer(createUserData))
    73  	err = tunnel.Wrap(func() error {
    74  		out, statusCode, err = g.DoHTTPThings(createUserRequest, g.Server.PushUsername, g.Server.PushPassword)
    75  		return err
    76  	})
    77  	message.Debugf("POST %s:\n%s", createUserEndpoint, string(out))
    78  	if err != nil {
    79  		if statusCode == 422 {
    80  			message.Debugf("Read-only git user already exists.  Skipping...")
    81  			return nil
    82  		}
    83  
    84  		return err
    85  	}
    86  
    87  	// Make sure the user can't create their own repos or orgs
    88  	updateUserBody := map[string]interface{}{
    89  		"login_name":                g.Server.PullUsername,
    90  		"max_repo_creation":         0,
    91  		"allow_create_organization": false,
    92  	}
    93  	updateUserData, _ := json.Marshal(updateUserBody)
    94  	updateUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users/%s", tunnelURL, g.Server.PullUsername)
    95  	updateUserRequest, _ := netHttp.NewRequest("PATCH", updateUserEndpoint, bytes.NewBuffer(updateUserData))
    96  	err = tunnel.Wrap(func() error {
    97  		out, _, err = g.DoHTTPThings(updateUserRequest, g.Server.PushUsername, g.Server.PushPassword)
    98  		return err
    99  	})
   100  	message.Debugf("PATCH %s:\n%s", updateUserEndpoint, string(out))
   101  	return err
   102  }
   103  
   104  // UpdateJackalGiteaUsers updates Jackal gitea users
   105  func (g *Git) UpdateJackalGiteaUsers(oldState *types.JackalState) error {
   106  
   107  	//Update git read only user password
   108  	err := g.UpdateGitUser(oldState.GitServer.PushPassword, g.Server.PullUsername, g.Server.PullPassword)
   109  	if err != nil {
   110  		return fmt.Errorf("unable to update gitea read only user password: %w", err)
   111  	}
   112  
   113  	// Update Git admin password
   114  	err = g.UpdateGitUser(oldState.GitServer.PushPassword, g.Server.PushUsername, g.Server.PushPassword)
   115  	if err != nil {
   116  		return fmt.Errorf("unable to update gitea admin user password: %w", err)
   117  	}
   118  	return nil
   119  }
   120  
   121  // UpdateGitUser updates Jackal git server users
   122  func (g *Git) UpdateGitUser(oldAdminPass string, username string, userpass string) error {
   123  	message.Debugf("git.UpdateGitUser()")
   124  
   125  	c, err := cluster.NewCluster()
   126  	if err != nil {
   127  		return err
   128  	}
   129  	// Establish a git tunnel to send the repo
   130  	tunnel, err := c.NewTunnel(cluster.JackalNamespaceName, k8s.SvcResource, cluster.JackalGitServerName, "", 0, cluster.JackalGitServerPort)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	_, err = tunnel.Connect()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	defer tunnel.Close()
   139  	tunnelURL := tunnel.HTTPEndpoint()
   140  
   141  	var out []byte
   142  
   143  	// Update the existing user's password
   144  	updateUserBody := map[string]interface{}{
   145  		"login_name": username,
   146  		"password":   userpass,
   147  	}
   148  	updateUserData, _ := json.Marshal(updateUserBody)
   149  	updateUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users/%s", tunnelURL, username)
   150  	updateUserRequest, _ := netHttp.NewRequest("PATCH", updateUserEndpoint, bytes.NewBuffer(updateUserData))
   151  	err = tunnel.Wrap(func() error {
   152  		out, _, err = g.DoHTTPThings(updateUserRequest, g.Server.PushUsername, oldAdminPass)
   153  		return err
   154  	})
   155  	message.Debugf("PATCH %s:\n%s", updateUserEndpoint, string(out))
   156  	return err
   157  }
   158  
   159  // CreatePackageRegistryToken uses the Gitea API to create a package registry token.
   160  func (g *Git) CreatePackageRegistryToken() (CreateTokenResponse, error) {
   161  	message.Debugf("git.CreatePackageRegistryToken()")
   162  
   163  	c, err := cluster.NewCluster()
   164  	if err != nil {
   165  		return CreateTokenResponse{}, err
   166  	}
   167  
   168  	// Establish a git tunnel to send the repo
   169  	tunnel, err := c.NewTunnel(cluster.JackalNamespaceName, k8s.SvcResource, cluster.JackalGitServerName, "", 0, cluster.JackalGitServerPort)
   170  	if err != nil {
   171  		return CreateTokenResponse{}, err
   172  	}
   173  	_, err = tunnel.Connect()
   174  	if err != nil {
   175  		return CreateTokenResponse{}, err
   176  	}
   177  	defer tunnel.Close()
   178  
   179  	tunnelURL := tunnel.Endpoint()
   180  
   181  	var out []byte
   182  
   183  	// Determine if the package token already exists
   184  	getTokensEndpoint := fmt.Sprintf("http://%s/api/v1/users/%s/tokens", tunnelURL, g.Server.PushUsername)
   185  	getTokensRequest, _ := netHttp.NewRequest("GET", getTokensEndpoint, nil)
   186  	err = tunnel.Wrap(func() error {
   187  		out, _, err = g.DoHTTPThings(getTokensRequest, g.Server.PushUsername, g.Server.PushPassword)
   188  		return err
   189  	})
   190  	message.Debugf("GET %s:\n%s", getTokensEndpoint, string(out))
   191  	if err != nil {
   192  		return CreateTokenResponse{}, err
   193  	}
   194  
   195  	hasPackageToken := false
   196  	var tokens []map[string]interface{}
   197  	err = json.Unmarshal(out, &tokens)
   198  	if err != nil {
   199  		return CreateTokenResponse{}, err
   200  	}
   201  
   202  	for _, token := range tokens {
   203  		if token["name"] == config.JackalArtifactTokenName {
   204  			hasPackageToken = true
   205  		}
   206  	}
   207  
   208  	if hasPackageToken {
   209  		// Delete the existing token to be replaced
   210  		deleteTokensEndpoint := fmt.Sprintf("http://%s/api/v1/users/%s/tokens/%s", tunnelURL, g.Server.PushUsername, config.JackalArtifactTokenName)
   211  		deleteTokensRequest, _ := netHttp.NewRequest("DELETE", deleteTokensEndpoint, nil)
   212  		err = tunnel.Wrap(func() error {
   213  			out, _, err = g.DoHTTPThings(deleteTokensRequest, g.Server.PushUsername, g.Server.PushPassword)
   214  			return err
   215  		})
   216  		message.Debugf("DELETE %s:\n%s", deleteTokensEndpoint, string(out))
   217  		if err != nil {
   218  			return CreateTokenResponse{}, err
   219  		}
   220  	}
   221  
   222  	createTokensEndpoint := fmt.Sprintf("http://%s/api/v1/users/%s/tokens", tunnelURL, g.Server.PushUsername)
   223  	createTokensBody := map[string]interface{}{
   224  		"name":   config.JackalArtifactTokenName,
   225  		"scopes": []string{"read:user", "read:package", "write:package"},
   226  	}
   227  	createTokensData, _ := json.Marshal(createTokensBody)
   228  	createTokensRequest, _ := netHttp.NewRequest("POST", createTokensEndpoint, bytes.NewBuffer(createTokensData))
   229  	err = tunnel.Wrap(func() error {
   230  		out, _, err = g.DoHTTPThings(createTokensRequest, g.Server.PushUsername, g.Server.PushPassword)
   231  		return err
   232  	})
   233  	message.Debugf("POST %s:\n%s", createTokensEndpoint, string(out))
   234  	if err != nil {
   235  		return CreateTokenResponse{}, err
   236  	}
   237  
   238  	createTokenResponse := CreateTokenResponse{}
   239  	err = json.Unmarshal(out, &createTokenResponse)
   240  	if err != nil {
   241  		return CreateTokenResponse{}, err
   242  	}
   243  
   244  	return createTokenResponse, nil
   245  }
   246  
   247  // UpdateGiteaPVC updates the existing Gitea persistent volume claim and tells Gitea whether to create or not.
   248  func UpdateGiteaPVC(shouldRollBack bool) (string, error) {
   249  	c, err := cluster.NewCluster()
   250  	if err != nil {
   251  		return "false", err
   252  	}
   253  
   254  	pvcName := os.Getenv("JACKAL_VAR_GIT_SERVER_EXISTING_PVC")
   255  	groupKind := schema.GroupKind{
   256  		Group: "",
   257  		Kind:  "PersistentVolumeClaim",
   258  	}
   259  	labels := map[string]string{"app.kubernetes.io/managed-by": "Helm"}
   260  	annotations := map[string]string{"meta.helm.sh/release-name": "jackal-gitea", "meta.helm.sh/release-namespace": "jackal"}
   261  
   262  	if shouldRollBack {
   263  		err = c.K8s.RemoveLabelsAndAnnotations(cluster.JackalNamespaceName, pvcName, groupKind, labels, annotations)
   264  		return "false", err
   265  	}
   266  
   267  	if pvcName == "data-jackal-gitea-0" {
   268  		err = c.K8s.AddLabelsAndAnnotations(cluster.JackalNamespaceName, pvcName, groupKind, labels, annotations)
   269  		return "true", err
   270  	}
   271  
   272  	return "false", err
   273  }
   274  
   275  // DoHTTPThings adds http request boilerplate and perform the request, checking for a successful response.
   276  func (g *Git) DoHTTPThings(request *netHttp.Request, username, secret string) ([]byte, int, error) {
   277  	message.Debugf("git.DoHttpThings()")
   278  
   279  	// Prep the request with boilerplate
   280  	client := &netHttp.Client{Timeout: time.Second * 20}
   281  	request.SetBasicAuth(username, secret)
   282  	request.Header.Add("accept", "application/json")
   283  	request.Header.Add("Content-Type", "application/json")
   284  
   285  	// Perform the request and get the response
   286  	response, err := client.Do(request)
   287  	if err != nil {
   288  		return []byte{}, 0, err
   289  	}
   290  	responseBody, _ := io.ReadAll(response.Body)
   291  
   292  	// If we get a 'bad' status code we will have no error, create a useful one to return
   293  	if response.StatusCode < 200 || response.StatusCode >= 300 {
   294  		err = fmt.Errorf("got status code of %d during http request with body of: %s", response.StatusCode, string(responseBody))
   295  		return []byte{}, response.StatusCode, err
   296  	}
   297  
   298  	return responseBody, response.StatusCode, nil
   299  }
   300  
   301  func (g *Git) addReadOnlyUserToRepo(tunnelURL, repo string) error {
   302  	message.Debugf("git.addReadOnlyUserToRepo()")
   303  
   304  	// Add the readonly user to the repo
   305  	addCollabBody := map[string]string{
   306  		"permission": "read",
   307  	}
   308  	addCollabData, err := json.Marshal(addCollabBody)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	// Send API request to add a user as a read-only collaborator to a repo
   314  	addCollabEndpoint := fmt.Sprintf("%s/api/v1/repos/%s/%s/collaborators/%s", tunnelURL, g.Server.PushUsername, repo, g.Server.PullUsername)
   315  	addCollabRequest, _ := netHttp.NewRequest("PUT", addCollabEndpoint, bytes.NewBuffer(addCollabData))
   316  	out, _, err := g.DoHTTPThings(addCollabRequest, g.Server.PushUsername, g.Server.PushPassword)
   317  	message.Debugf("PUT %s:\n%s", addCollabEndpoint, string(out))
   318  	return err
   319  }