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 }