github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/pkg/git/client.go (about) 1 package git 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strings" 12 "time" 13 14 "errors" 15 16 "github.com/go-git/go-billy/v5" 17 "github.com/go-git/go-billy/v5/memfs" 18 gogit "github.com/go-git/go-git/v5" 19 "github.com/go-git/go-git/v5/config" 20 "github.com/go-git/go-git/v5/plumbing" 21 "github.com/go-git/go-git/v5/plumbing/object" 22 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" 23 "github.com/go-git/go-git/v5/storage/memory" 24 "golang.org/x/crypto/ssh" 25 "gopkg.in/yaml.v3" 26 27 "github.com/caos/orbos/internal/operator/common" 28 "github.com/caos/orbos/mntr" 29 "github.com/caos/orbos/pkg/tree" 30 ) 31 32 type DesiredFile string 33 34 func (d DesiredFile) WOExtension() string { 35 return strings.Split(string(d), ".")[0] 36 } 37 38 const ( 39 writeCheckTag = "writecheck" 40 41 OrbiterFile DesiredFile = "orbiter.yml" 42 BoomFile DesiredFile = "boom.yml" 43 NetworkingFile DesiredFile = "networking.yml" 44 DatabaseFile DesiredFile = "database.yml" 45 ZitadelFile DesiredFile = "zitadel.yml" 46 ) 47 48 type Client struct { 49 monitor mntr.Monitor 50 ctx context.Context 51 committer string 52 email string 53 auth *gitssh.PublicKeys 54 repo *gogit.Repository 55 fs billy.Filesystem 56 storage *memory.Storage 57 workTree *gogit.Worktree 58 progress io.Writer 59 repoURL string 60 cloned bool 61 } 62 63 func New(ctx context.Context, monitor mntr.Monitor, committer, email string) *Client { 64 newClient := &Client{ 65 ctx: ctx, 66 committer: committer, 67 email: email, 68 monitor: monitor, 69 storage: memory.NewStorage(), 70 fs: memfs.New(), 71 } 72 73 if monitor.IsVerbose() { 74 newClient.progress = os.Stdout 75 } 76 return newClient 77 } 78 79 func (g *Client) GetURL() string { 80 return g.repoURL 81 } 82 83 func (g *Client) Configure(repoURL string, deploykey []byte) error { 84 signer, err := ssh.ParsePrivateKey(deploykey) 85 if err != nil { 86 return mntr.ToUserError(fmt.Errorf("parsing deployment key failed: %w", err)) 87 } 88 89 if repoURL != g.repoURL { 90 g.repoURL = repoURL 91 g.cloned = false 92 } 93 g.monitor = g.monitor.WithField("repository", repoURL) 94 95 g.auth = &gitssh.PublicKeys{ 96 User: "git", 97 Signer: signer, 98 } 99 100 // TODO: Fix 101 g.auth.HostKeyCallback = ssh.InsecureIgnoreHostKey() 102 103 return nil 104 } 105 106 func (g *Client) Check() error { 107 if !g.cloned { 108 return nil 109 } 110 if err := g.readCheck(); err != nil { 111 return err 112 } 113 114 return g.writeCheck() 115 } 116 117 func (g *Client) readCheck() error { 118 119 rem := gogit.NewRemote(memory.NewStorage(), &config.RemoteConfig{ 120 Name: "origin", 121 URLs: []string{g.repoURL}, 122 }) 123 124 // We can then use every Remote functions to retrieve wanted information 125 _, err := rem.List(&gogit.ListOptions{ 126 Auth: g.auth, 127 }) 128 if err != nil { 129 return mntr.ToUserError(fmt.Errorf("read check failed: %w", err)) 130 } 131 132 g.monitor.Info("Read check success") 133 return nil 134 } 135 136 func (g *Client) writeCheck() (err error) { 137 138 defer func() { 139 err = mntr.ToUserError(err) 140 }() 141 142 head, err := g.repo.Head() 143 if err != nil { 144 return fmt.Errorf("failed to get head: %w", err) 145 } 146 localWriteCheckTag := strings.Join([]string{writeCheckTag, g.committer}, "-") 147 148 ref, createErr := g.repo.CreateTag(localWriteCheckTag, head.Hash(), nil) 149 if createErr == gogit.ErrTagExists { 150 if ref, err = g.repo.Tag(localWriteCheckTag); err != nil { 151 return err 152 } 153 createErr = nil 154 } 155 156 if createErr != nil { 157 return fmt.Errorf("write-check failed: %w", createErr) 158 } 159 160 if pushErr := g.repo.Push(&gogit.PushOptions{ 161 RemoteName: "origin", 162 RefSpecs: []config.RefSpec{ 163 config.RefSpec("+" + ref.Name() + ":" + ref.Name()), 164 }, 165 Auth: g.auth, 166 }); pushErr != nil && pushErr != gogit.NoErrAlreadyUpToDate { 167 return fmt.Errorf("write-check failed: %w", pushErr) 168 } 169 170 g.monitor.Debug("Write check tag created") 171 172 if deleteErr := g.repo.DeleteTag(localWriteCheckTag); deleteErr != nil && deleteErr != gogit.ErrTagNotFound { 173 return fmt.Errorf("write-check cleanup delete tag failed: %w", deleteErr) 174 } 175 176 if err := g.repo.Push(&gogit.PushOptions{ 177 RemoteName: "origin", 178 RefSpecs: []config.RefSpec{ 179 config.RefSpec(":" + ref.Name()), 180 }, 181 Auth: g.auth, 182 }); err != nil { 183 return fmt.Errorf("write-check cleanup failed: %w", err) 184 } 185 186 g.monitor.Debug("Write check tag cleaned up") 187 g.monitor.Info("Write check success") 188 return nil 189 } 190 191 func (g *Client) Clone() (err error) { 192 for i := 0; i < 10; i++ { 193 if err = g.clone(); err == nil { 194 return nil 195 } 196 time.Sleep(time.Second) 197 } 198 return err 199 } 200 201 func (g *Client) clone() error { 202 g.fs = memfs.New() 203 204 g.monitor.Debug("Cloning") 205 var err error 206 g.repo, err = gogit.CloneContext(g.ctx, memory.NewStorage(), g.fs, &gogit.CloneOptions{ 207 URL: g.repoURL, 208 Auth: g.auth, 209 SingleBranch: true, 210 Depth: 1, 211 Progress: g.progress, 212 }) 213 if err != nil { 214 return mntr.ToUserError(fmt.Errorf("cloning repository from %s failed: %w", g.repoURL, err)) 215 } 216 g.monitor.Debug("Cloned") 217 218 g.workTree, err = g.repo.Worktree() 219 if err != nil { 220 panic(err) 221 } 222 223 g.cloned = true 224 225 return nil 226 } 227 228 func (g *Client) Read(path string) []byte { 229 230 readmonitor := g.monitor.WithFields(map[string]interface{}{ 231 "path": path, 232 }) 233 readmonitor.Debug("Reading file") 234 file, err := g.fs.Open(path) 235 if err != nil { 236 if os.IsNotExist(err) { 237 return make([]byte, 0) 238 } 239 panic(err) 240 } 241 defer file.Close() 242 fileBytes, err := ioutil.ReadAll(file) 243 if err != nil { 244 panic(err) 245 } 246 if readmonitor.IsVerbose() { 247 readmonitor.Debug("File read") 248 fmt.Println(string(fileBytes)) 249 } 250 return fileBytes 251 } 252 253 func (g *Client) ReadYamlIntoStruct(path string, struc interface{}) error { 254 data := g.Read(path) 255 256 err := yaml.Unmarshal(data, struc) 257 if err != nil { 258 err = fmt.Errorf("unmarshaling yaml %s to struct failed: %w", path, err) 259 } 260 261 return err 262 } 263 264 func (g *Client) ExistsFolder(path string) (bool, error) { 265 monitor := g.monitor.WithFields(map[string]interface{}{ 266 "path": path, 267 }) 268 monitor.Debug("Reading folder") 269 _, err := g.fs.ReadDir(path) 270 if err != nil { 271 if os.IsNotExist(err) { 272 return false, nil 273 } 274 return false, fmt.Errorf("opening %s from worktree failed: %w", path, err) 275 } 276 277 return true, nil 278 } 279 280 func (g *Client) EmptyFolder(path string) (bool, error) { 281 monitor := g.monitor.WithFields(map[string]interface{}{ 282 "path": path, 283 }) 284 monitor.Debug("Reading folder") 285 files, err := g.fs.ReadDir(path) 286 if err != nil { 287 return false, fmt.Errorf("opening %s from worktree failed: %w", path, err) 288 } 289 if len(files) == 0 { 290 return true, nil 291 } 292 return false, nil 293 } 294 295 func (g *Client) ReadFolder(path string) (map[string][]byte, []string, error) { 296 monitor := g.monitor.WithFields(map[string]interface{}{ 297 "path": path, 298 }) 299 monitor.Debug("Reading folder") 300 dirBytes := make(map[string][]byte, 0) 301 files, err := g.fs.ReadDir(path) 302 if err != nil { 303 if os.IsNotExist(err) { 304 return make(map[string][]byte, 0), nil, nil 305 } 306 return nil, nil, fmt.Errorf("opening %s from worktree failed: %w", path, err) 307 } 308 subdirs := make([]string, 0) 309 for _, file := range files { 310 if !file.IsDir() { 311 filePath := filepath.Join(path, file.Name()) 312 fileBytes := g.Read(filePath) 313 dirBytes[file.Name()] = fileBytes 314 } else { 315 subdirs = append(subdirs, file.Name()) 316 } 317 } 318 319 if monitor.IsVerbose() { 320 monitor.Debug("Folder read") 321 fmt.Println(dirBytes) 322 } 323 return dirBytes, subdirs, nil 324 } 325 326 type File struct { 327 Path string 328 Content []byte 329 } 330 331 func (g *Client) stageAndCommit(msg string, files ...File) (bool, error) { 332 if g.stage(files...) { 333 return false, nil 334 } 335 336 return true, g.Commit(msg) 337 } 338 339 func (g *Client) UpdateRemote(msg string, whenCloned func() []File) error { 340 341 if err := g.Clone(); err != nil { 342 return fmt.Errorf("recloning before committing changes failed: %w", err) 343 } 344 345 changed, err := g.stageAndCommit(msg, whenCloned()...) 346 if err != nil { 347 return err 348 } 349 350 if !changed { 351 g.monitor.Info("No changes") 352 return nil 353 } 354 err = g.Push() 355 if err != nil && 356 (errors.Is(err, plumbing.ErrObjectNotFound) || 357 strings.Contains(err.Error(), "cannot lock ref")) { 358 g.monitor.WithField("response", err.Error()).Info("Git collision detected, retrying") 359 return g.UpdateRemote(msg, whenCloned) 360 } 361 return err 362 } 363 364 func (g *Client) stage(files ...File) bool { 365 for _, f := range files { 366 updatemonitor := g.monitor.WithFields(map[string]interface{}{ 367 "path": f.Path, 368 }) 369 370 updatemonitor.Debug("Overwriting local index") 371 372 file, err := g.fs.Create(f.Path) 373 if err != nil { 374 panic(err) 375 } 376 //noinspection GoDeferInLoop 377 defer file.Close() 378 379 if _, err := io.Copy(file, bytes.NewReader(f.Content)); err != nil { 380 panic(err) 381 } 382 383 _, err = g.workTree.Add(f.Path) 384 if err != nil { 385 panic(err) 386 } 387 } 388 389 status, err := g.workTree.Status() 390 if err != nil { 391 panic(err) 392 } 393 394 return status.IsClean() 395 } 396 397 func (g *Client) Commit(msg string) error { 398 399 if _, err := g.workTree.Commit(msg, &gogit.CommitOptions{ 400 Author: &object.Signature{ 401 Name: g.committer, 402 Email: g.email, 403 When: time.Now(), 404 }, 405 }); err != nil { 406 return fmt.Errorf("committing changes failed: %w", err) 407 } 408 g.monitor.Debug("Changes commited") 409 return nil 410 } 411 412 func (g *Client) Push() error { 413 414 if err := g.repo.PushContext(g.ctx, &gogit.PushOptions{ 415 RemoteName: "origin", 416 // RefSpecs: refspecs, 417 Auth: g.auth, 418 Progress: g.progress, 419 }); err != nil { 420 return fmt.Errorf("pushing repository failed: %w", err) 421 } 422 423 g.monitor.Info("Repository pushed") 424 return nil 425 } 426 427 func (g *Client) Exists(path DesiredFile) bool { 428 of := g.Read(string(path)) 429 if of != nil && len(of) > 0 { 430 return true 431 } 432 return false 433 } 434 435 func (g *Client) ReadTree(path DesiredFile) (*tree.Tree, error) { 436 tree := &tree.Tree{} 437 return tree, yaml.Unmarshal(g.Read(string(path)), tree) 438 } 439 440 type GitDesiredState struct { 441 Desired *tree.Tree 442 Path DesiredFile 443 } 444 445 func (g *Client) PushGitDesiredStates(monitor mntr.Monitor, msg string, desireds []GitDesiredState) (err error) { 446 monitor.OnChange = func(_ string, fields map[string]string) { 447 err = g.UpdateRemote(mntr.SprintCommit(msg, fields), func() []File { 448 gitFiles := make([]File, len(desireds)) 449 for i := range desireds { 450 desired := desireds[i] 451 gitFiles[i] = File{ 452 Path: string(desired.Path), 453 Content: common.MarshalYAML(desired.Desired), 454 } 455 } 456 return gitFiles 457 }) 458 } 459 monitor.Changed(msg) 460 return err 461 } 462 463 func (g *Client) PushDesiredFunc(file DesiredFile, desired *tree.Tree) func(mntr.Monitor) error { 464 return func(monitor mntr.Monitor) error { 465 monitor.WithField("file", file).Info("Writing desired state") 466 return g.PushGitDesiredStates(monitor, fmt.Sprintf("Desired state written to %s", file), []GitDesiredState{{ 467 Desired: desired, 468 Path: file, 469 }}) 470 } 471 }