From 0803a753c788fe50c1286f142193cfd1b7dd8d60 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Tue, 21 May 2024 20:32:35 +0300 Subject: [PATCH] Add Fly.io provider Signed-off-by: bcmmbaga --- README.md | 22 ++- go.mod | 24 ++- go.sum | 70 ++++++++- pkg/provider/provider.go | 233 +++++++++++++++++++---------- pkg/provider/provider_test.go | 144 ++++++++---------- pkg/provider/util/fly.go | 269 ++++++++++++++++++++++++++++++++++ pkg/types/metadata.go | 7 +- pkg/types/targets.go | 58 ++++++-- pkg/types/targets_test.go | 74 ++++++++++ 9 files changed, 706 insertions(+), 195 deletions(-) create mode 100644 pkg/provider/util/fly.go create mode 100644 pkg/types/targets_test.go diff --git a/README.md b/README.md index 94c2644..87a7a0e 100644 --- a/README.md +++ b/README.md @@ -24,25 +24,21 @@ This repository is the home of the X

-> [!TIP] -> Write a description of your Provider here. +The Fly Provider allows Daytona to create workspace projects on Fly VMs known as Fly Machines. ## Target Options -| Property | Type | Optional | DefaultValue | InputMasked | DisabledPredicate | -|------------------------- |---------- |---------- |----------------------------- |------------- |------------------- | -| Required String | String | false | default-required-string | false | | -| Optional String | String | true | | true | | -| Optional Int | Int | true | | false | | -| FilePath | FilePath | true | ~/.ssh | false | ^default-target$ | +| Property | Type | Optional | DefaultValue | InputMasked | DisabledPredicate | +|----------------------|---------------|-------------|---------------|-----------------|---------------------| +| AuthToken | String | false | | true | | +| OrgSlug | String | false | | false | | +| Region | String | true | | false | | +| DiskSize | String | true | 10 | false | | +| Size | String | true | shared-cpu-4x | false | | ### Default Targets -#### Local -| Property | Value | -|----------------- |----------------------------- | -| Required String | default-required-string | - +The Fly Provider has no default targets. Before using the provider you must set the target using the `daytona target set` command. ## Code of Conduct diff --git a/go.mod b/go.mod index 26b4a79..d283fbd 100644 --- a/go.mod +++ b/go.mod @@ -7,20 +7,29 @@ require ( github.com/hashicorp/go-hclog v1.6.2 github.com/hashicorp/go-plugin v1.6.0 github.com/sirupsen/logrus v1.9.3 + github.com/superfly/fly-go v0.1.12 ) require ( code.gitea.io/sdk/gitea v0.17.1 // indirect + github.com/Khan/genqlient v0.6.0 // indirect + github.com/PuerkitoBio/rehttp v1.4.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/fatih/color v1.15.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/ktrysmt/go-bitbucket v0.9.76 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -28,21 +37,32 @@ require ( github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/superfly/graphql v0.2.4 // indirect + github.com/superfly/macaroon v0.2.12 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.1 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xanzy/go-gitlab v0.97.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/grpc v1.63.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index d3dd4ff..15dd522 100644 --- a/go.sum +++ b/go.sum @@ -602,20 +602,37 @@ git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3p github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= +github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/rehttp v1.4.0 h1:rIN7A2s+O9fmHUM1vUcInvlHj9Ysql4hE+Y0wcl/xk8= +github.com/PuerkitoBio/rehttp v1.4.0/go.mod h1:LUwKPoDbDIA2RL5wYZCNsQ90cx4OJ4AWBmq6KzWZL1s= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -668,6 +685,8 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -683,6 +702,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -778,6 +802,8 @@ github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -815,8 +841,12 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -845,6 +875,8 @@ github.com/ktrysmt/go-bitbucket v0.9.76/go.mod h1:+wfH5IgKupLt9U0Voy1nH3YX+kNVtd github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -870,6 +902,7 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -886,6 +919,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -909,10 +945,22 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/superfly/fly-go v0.1.12 h1:Ql+wbDFuiWEGK+qskuSTtMft/zfh1rIBp/1Dcu7JbgQ= +github.com/superfly/fly-go v0.1.12/go.mod h1:0wgmVsqPPDgPSTsZA0L0zI5uajxx86KfuBoPQT87B1E= +github.com/superfly/graphql v0.2.4 h1:Av8hSk4x8WvKJ6MTnEwrLknSVSGPc7DWpgT3z/kt3PU= +github.com/superfly/graphql v0.2.4/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc= +github.com/superfly/macaroon v0.2.12 h1:fxIHs4Cd8J8uTXqGgYcAylFltjvMeIU1JBTyL3q70gc= +github.com/superfly/macaroon v0.2.12/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= +github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/go-gitlab v0.97.0 h1:StMqJ1Kvt00X43pYIBBjj52dFlghwSeBhRDRfzaZ7xY= github.com/xanzy/go-gitlab v0.97.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -934,6 +982,14 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -970,6 +1026,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1050,6 +1108,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1211,8 +1270,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1608,14 +1667,17 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index ed5046b..e9a0f4d 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -2,16 +2,19 @@ package provider import ( "encoding/json" + "errors" "io" + "time" - internal "github.com/daytonaio/daytona-provider-fly/internal" - log_writers "github.com/daytonaio/daytona-provider-fly/internal/log" - provider_types "github.com/daytonaio/daytona-provider-fly/pkg/types" - + "github.com/daytonaio/daytona-provider-fly/internal" + logwriters "github.com/daytonaio/daytona-provider-fly/internal/log" + flyutil "github.com/daytonaio/daytona-provider-fly/pkg/provider/util" + "github.com/daytonaio/daytona-provider-fly/pkg/types" "github.com/daytonaio/daytona/pkg/logger" "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/provider/util" "github.com/daytonaio/daytona/pkg/workspace" + "github.com/superfly/fly-go" ) type FlyProvider struct { @@ -19,15 +22,13 @@ type FlyProvider struct { ServerDownloadUrl *string ServerVersion *string ServerUrl *string + NetworkKey *string ServerApiUrl *string LogsDir *string - NetworkKey *string - OwnProperty string } +// Initialize initializes the provider with the given configuration. func (p *FlyProvider) Initialize(req provider.InitializeProviderRequest) (*util.Empty, error) { - p.OwnProperty = "my-own-property" - p.BasePath = &req.BasePath p.ServerDownloadUrl = &req.ServerDownloadUrl p.ServerVersion = &req.ServerVersion @@ -39,71 +40,44 @@ func (p *FlyProvider) Initialize(req provider.InitializeProviderRequest) (*util. return new(util.Empty), nil } -func (p FlyProvider) GetInfo() (provider.ProviderInfo, error) { +// GetInfo returns the provider information. +func (p *FlyProvider) GetInfo() (provider.ProviderInfo, error) { return provider.ProviderInfo{ Name: "fly-provider", Version: internal.Version, }, nil } -func (p FlyProvider) GetTargetManifest() (*provider.ProviderTargetManifest, error) { - return provider_types.GetTargetManifest(), nil +func (p *FlyProvider) GetTargetManifest() (*provider.ProviderTargetManifest, error) { + return types.GetTargetManifest(), nil } -func (p FlyProvider) GetDefaultTargets() (*[]provider.ProviderTarget, error) { - info, err := p.GetInfo() - if err != nil { - return nil, err - } - - defaultTargets := []provider.ProviderTarget{ - { - Name: "default-target", - ProviderInfo: info, - Options: "{\n\t\"Required String\": \"default-required-string\"\n}", - }, - } - return &defaultTargets, nil +func (p *FlyProvider) GetDefaultTargets() (*[]provider.ProviderTarget, error) { + return new([]provider.ProviderTarget), nil } -func (p FlyProvider) CreateWorkspace(workspaceReq *provider.WorkspaceRequest) (*util.Empty, error) { - logWriter := io.MultiWriter(&log_writers.InfoLogWriter{}) - if p.LogsDir != nil { - loggerFactory := logger.NewLoggerFactory(*p.LogsDir) - wsLogWriter := loggerFactory.CreateWorkspaceLogger(workspaceReq.Workspace.Id) - logWriter = io.MultiWriter(&log_writers.InfoLogWriter{}, wsLogWriter) - defer wsLogWriter.Close() - } - - logWriter.Write([]byte("Workspace created\n")) - +func (p *FlyProvider) CreateWorkspace(_ *provider.WorkspaceRequest) (*util.Empty, error) { return new(util.Empty), nil } -func (p FlyProvider) StartWorkspace(workspaceReq *provider.WorkspaceRequest) (*util.Empty, error) { +func (p *FlyProvider) StartWorkspace(_ *provider.WorkspaceRequest) (*util.Empty, error) { return new(util.Empty), nil } -func (p FlyProvider) StopWorkspace(workspaceReq *provider.WorkspaceRequest) (*util.Empty, error) { +func (p *FlyProvider) StopWorkspace(_ *provider.WorkspaceRequest) (*util.Empty, error) { return new(util.Empty), nil } -func (p FlyProvider) DestroyWorkspace(workspaceReq *provider.WorkspaceRequest) (*util.Empty, error) { +func (p *FlyProvider) DestroyWorkspace(_ *provider.WorkspaceRequest) (*util.Empty, error) { return new(util.Empty), nil } -func (p FlyProvider) GetWorkspaceInfo(workspaceReq *provider.WorkspaceRequest) (*workspace.WorkspaceInfo, error) { - providerMetadata, err := p.getWorkspaceMetadata(workspaceReq) - if err != nil { - return nil, err - } - +func (p *FlyProvider) GetWorkspaceInfo(workspaceReq *provider.WorkspaceRequest) (*workspace.WorkspaceInfo, error) { workspaceInfo := &workspace.WorkspaceInfo{ - Name: workspaceReq.Workspace.Name, - ProviderMetadata: providerMetadata, + Name: workspaceReq.Workspace.Name, } - projectInfos := []*workspace.ProjectInfo{} + var projectInfos []*workspace.ProjectInfo for _, project := range workspaceReq.Workspace.Projects { projectInfo, err := p.GetProjectInfo(&provider.ProjectRequest{ TargetOptions: workspaceReq.TargetOptions, @@ -119,61 +93,168 @@ func (p FlyProvider) GetWorkspaceInfo(workspaceReq *provider.WorkspaceRequest) ( return workspaceInfo, nil } -func (p FlyProvider) CreateProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { - logWriter := io.MultiWriter(&log_writers.InfoLogWriter{}) - if p.LogsDir != nil { - loggerFactory := logger.NewLoggerFactory(*p.LogsDir) - projectLogWriter := loggerFactory.CreateProjectLogger(projectReq.Project.WorkspaceId, projectReq.Project.Name) - logWriter = io.MultiWriter(&log_writers.InfoLogWriter{}, projectLogWriter) - defer projectLogWriter.Close() +func (p *FlyProvider) CreateProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { + if p.ServerDownloadUrl == nil { + return nil, errors.New("serverDownloadUrl not set. Did you forget to call Initialize") + } + logWriter, cleanupFunc := p.getLogWriter(projectReq.Project.WorkspaceId, projectReq.Project.Name) + + targetOptions, err := types.ParseTargetOptions(projectReq.TargetOptions) + if err != nil { + logWriter.Write([]byte("Failed to parse target options: " + err.Error() + "\n")) + return nil, err } + initScript := util.GetProjectStartScript(*p.ServerDownloadUrl, projectReq.Project.ApiKey) + machine, err := flyutil.CreateMachine(projectReq.Project, targetOptions, initScript) + if err != nil { + logWriter.Write([]byte("Failed to create machine: " + err.Error() + "\n")) + return nil, err + } logWriter.Write([]byte("Project created\n")) + if err := checkMachineReady(projectReq, targetOptions); err != nil { + logWriter.Write([]byte("Failed to check machine status: " + err.Error() + "\n")) + return nil, err + } + + go func() { + if err := flyutil.GetMachineLogs(projectReq.Project, targetOptions, machine.ID, logWriter); err != nil { + logWriter.Write([]byte(err.Error())) + defer cleanupFunc() + } + }() + return new(util.Empty), nil } -func (p FlyProvider) StartProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { +func (p *FlyProvider) StartProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { + logWriter, cleanupFunc := p.getLogWriter(projectReq.Project.WorkspaceId, projectReq.Project.Name) + defer cleanupFunc() + + targetOptions, err := types.ParseTargetOptions(projectReq.TargetOptions) + if err != nil { + logWriter.Write([]byte("Failed to parse target options: " + err.Error() + "\n")) + return nil, err + } + + if err := flyutil.StartMachine(projectReq.Project, targetOptions); err != nil { + logWriter.Write([]byte("Failed to start machine: " + err.Error() + "\n")) + return nil, err + } + logWriter.Write([]byte("Project started.\n")) + return new(util.Empty), nil } -func (p FlyProvider) StopProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { +func (p *FlyProvider) StopProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { + logWriter, cleanupFunc := p.getLogWriter(projectReq.Project.WorkspaceId, projectReq.Project.Name) + defer cleanupFunc() + + targetOptions, err := types.ParseTargetOptions(projectReq.TargetOptions) + if err != nil { + logWriter.Write([]byte("Failed to parse target options: " + err.Error() + "\n")) + return nil, err + } + + if err := flyutil.StopMachine(projectReq.Project, targetOptions); err != nil { + logWriter.Write([]byte("Failed to stop machine: " + err.Error() + "\n")) + return nil, err + } + logWriter.Write([]byte("Project stopped.\n")) + return new(util.Empty), nil } -func (p FlyProvider) DestroyProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { +func (p *FlyProvider) DestroyProject(projectReq *provider.ProjectRequest) (*util.Empty, error) { + logWriter, cleanupFunc := p.getLogWriter(projectReq.Project.WorkspaceId, projectReq.Project.Name) + defer cleanupFunc() + + targetOptions, err := types.ParseTargetOptions(projectReq.TargetOptions) + if err != nil { + logWriter.Write([]byte("Failed to parse target options: " + err.Error() + "\n")) + return nil, err + } + + if err := flyutil.DeleteMachine(projectReq.Project, targetOptions); err != nil { + logWriter.Write([]byte("Failed to destroy machine: " + err.Error() + "\n")) + return nil, err + } + logWriter.Write([]byte("Project destroyed.\n")) + return new(util.Empty), nil } -func (p FlyProvider) GetProjectInfo(projectReq *provider.ProjectRequest) (*workspace.ProjectInfo, error) { - providerMetadata := provider_types.ProjectMetadata{ - Property: projectReq.Project.Name, +func (p *FlyProvider) GetProjectInfo(projectReq *provider.ProjectRequest) (*workspace.ProjectInfo, error) { + return p.getProjectInfo(projectReq) +} + +func (p *FlyProvider) getProjectInfo(projectReq *provider.ProjectRequest) (*workspace.ProjectInfo, error) { + logWriter, cleanupFunc := p.getLogWriter(projectReq.Project.WorkspaceId, projectReq.Project.Name) + defer cleanupFunc() + + targetOptions, err := types.ParseTargetOptions(projectReq.TargetOptions) + if err != nil { + logWriter.Write([]byte("Failed to parse target options: " + err.Error() + "\n")) + return nil, err } - metadataString, err := json.Marshal(providerMetadata) + machine, err := flyutil.GetMachine(projectReq.Project, targetOptions) if err != nil { + logWriter.Write([]byte("Failed to get machine: " + err.Error() + "\n")) return nil, err } - projectInfo := &workspace.ProjectInfo{ - Name: projectReq.Project.Name, - IsRunning: true, - Created: "Created at ...", - ProviderMetadata: string(metadataString), + metadata := types.ProjectMetadata{ + MachineId: machine.ID, + VolumeId: machine.Config.Mounts[0].Volume, } - return projectInfo, nil + jsonMetadata, err := json.Marshal(metadata) + if err != nil { + return nil, err + } + + return &workspace.ProjectInfo{ + Name: projectReq.Project.Name, + IsRunning: machine.State == fly.MachineStateStarted, + Created: machine.CreatedAt, + ProviderMetadata: string(jsonMetadata), + }, nil } -func (p FlyProvider) getWorkspaceMetadata(workspaceReq *provider.WorkspaceRequest) (string, error) { - metadata := provider_types.WorkspaceMetadata{ - Property: workspaceReq.Workspace.Id, - } +func (p *FlyProvider) getLogWriter(workspaceId string, projectName string) (io.Writer, func()) { + logWriter := io.MultiWriter(&logwriters.InfoLogWriter{}) + cleanupFunc := func() {} - jsonContent, err := json.Marshal(metadata) - if err != nil { - return "", err + if p.LogsDir != nil { + loggerFactory := logger.NewLoggerFactory(*p.LogsDir) + projectLogWriter := loggerFactory.CreateProjectLogger(workspaceId, projectName) + logWriter = io.MultiWriter(&logwriters.InfoLogWriter{}, projectLogWriter) + cleanupFunc = func() { projectLogWriter.Close() } } - return string(jsonContent), nil + return logWriter, cleanupFunc +} + +// checkMachineReady checks if the machine for the given workspace is ready to use. +// It polls the status periodically until the machine is ready or a timeout is reached. +func checkMachineReady(projectReq *provider.ProjectRequest, targetOptions *types.TargetOptions) error { + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + + end := time.Now().Add(5 * time.Minute) + + for { + select { + case <-ticker.C: + if flyutil.IsMachineReady(projectReq.Project, targetOptions) { + return nil + } + default: + if time.Now().After(end) { + return errors.New("machine was not ready within 5 minutes") + } + } + } } diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go index acfea02..2e371f9 100644 --- a/pkg/provider/provider_test.go +++ b/pkg/provider/provider_test.go @@ -1,127 +1,112 @@ -package provider_test +package provider import ( "encoding/json" + "os" "testing" + "time" + flyutil "github.com/daytonaio/daytona-provider-fly/pkg/provider/util" + "github.com/daytonaio/daytona-provider-fly/pkg/types" "github.com/daytonaio/daytona/pkg/gitprovider" - daytona_provider "github.com/daytonaio/daytona/pkg/provider" + "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/workspace" - - "github.com/daytonaio/daytona-provider-fly/pkg/provider" - provider_types "github.com/daytonaio/daytona-provider-fly/pkg/types" ) -var sampleProvider = &provider.FlyProvider{} -var targetOptions = &provider_types.TargetOptions{ - RequiredString: "default-required-string", -} -var optionsString string - -var project1 = &workspace.Project{ - Name: "test", - Repository: &gitprovider.GitRepository{ - Id: "123", - Url: "https://github.com/daytonaio/daytona", - Name: "daytona", - }, - WorkspaceId: "123", - EnvVars: map[string]string{ - "DAYTONA_WS_ID": "123", - "DAYTONA_WS_PROJECT_NAME": "test", - "DAYTONA_WS_PROJECT_REPOSITORY_URL": "https://github.com/daytonaio/daytona", - "DAYTONA_SERVER_API_KEY": "api-key-test", - "DAYTONA_SERVER_VERSION": "latest", - "DAYTONA_SERVER_URL": "http://localhost:3001", - "DAYTONA_SERVER_API_URL": "http://localhost:3000", - }, -} - -var workspace1 = &workspace.Workspace{ - Id: "123", - Name: "test", - Target: "local", - Projects: []*workspace.Project{ - project1, - }, -} - -func TestCreateWorkspace(t *testing.T) { - wsReq := &daytona_provider.WorkspaceRequest{ - TargetOptions: optionsString, - Workspace: workspace1, +var ( + orgSlug = os.Getenv("FLY_TEST_ORG_SLUG") + authToken = os.Getenv("FLY_TEST_ACCESS_TOKEN") + optionsString string + + flyProvider = &FlyProvider{} + targetOptions = &types.TargetOptions{ + Region: "lax", + Size: "shared-cpu-4x", + DiskSize: 10, + OrgSlug: orgSlug, + AuthToken: &authToken, } - _, err := sampleProvider.CreateWorkspace(wsReq) - if err != nil { - t.Errorf("Error creating workspace: %s", err) + project1 = &workspace.Project{ + Name: "test", + Repository: &gitprovider.GitRepository{ + Id: "123", + Url: "https://github.com/daytonaio/daytona", + Name: "daytona", + }, + Image: "daytonaio/workspace-project:latest", + WorkspaceId: "123", } -} +) -func TestGetWorkspaceInfo(t *testing.T) { - wsReq := &daytona_provider.WorkspaceRequest{ +func TestCreateProject(t *testing.T) { + projectReq := &provider.ProjectRequest{ TargetOptions: optionsString, - Workspace: workspace1, - } - - workspaceInfo, err := sampleProvider.GetWorkspaceInfo(wsReq) - if err != nil || workspaceInfo == nil { - t.Errorf("Error getting workspace info: %s", err) + Project: project1, } - var workspaceMetadata provider_types.WorkspaceMetadata - err = json.Unmarshal([]byte(workspaceInfo.ProviderMetadata), &workspaceMetadata) + _, err := flyProvider.CreateProject(projectReq) if err != nil { - t.Errorf("Error unmarshalling workspace metadata: %s", err) + t.Fatalf("Error creating project: %s", err) } - if workspaceMetadata.Property != wsReq.Workspace.Id { - t.Errorf("Expected network id %s, got %s", wsReq.Workspace.Id, workspaceMetadata.Property) + _, err = flyutil.GetMachine(project1, targetOptions) + if err != nil { + t.Fatalf("Error getting machine: %s", err) } } -func TestDestroyWorkspace(t *testing.T) { - wsReq := &daytona_provider.WorkspaceRequest{ +func TestProjectInfo(t *testing.T) { + projectReq := &provider.ProjectRequest{ TargetOptions: optionsString, - Workspace: workspace1, + Project: project1, } - _, err := sampleProvider.DestroyWorkspace(wsReq) + projectInfo, err := flyProvider.GetProjectInfo(projectReq) if err != nil { - t.Errorf("Error deleting workspace: %s", err) + t.Fatalf("Error getting workspace info: %s", err) } -} -func TestCreateProject(t *testing.T) { - TestCreateWorkspace(t) - - projectReq := &daytona_provider.ProjectRequest{ - TargetOptions: optionsString, - Project: project1, + var projectMetadata types.ProjectMetadata + err = json.Unmarshal([]byte(projectInfo.ProviderMetadata), &projectMetadata) + if err != nil { + t.Fatalf("Error unmarshalling project metadata: %s", err) } - _, err := sampleProvider.CreateProject(projectReq) + machine, err := flyutil.GetMachine(project1, targetOptions) if err != nil { - t.Errorf("Error creating project: %s", err) + t.Fatalf("Error getting machine: %s", err) + } + + if projectMetadata.MachineId != machine.ID { + t.Fatalf("Expected machine id %s, got %s", projectMetadata.MachineId, machine.ID) + } + + if projectMetadata.VolumeId != machine.Config.Mounts[0].Volume { + t.Fatalf("Expected volume id %s, got %s", projectMetadata.VolumeId, machine.Config.Mounts[0].Volume) } } func TestDestroyProject(t *testing.T) { - projectReq := &daytona_provider.ProjectRequest{ + projectReq := &provider.ProjectRequest{ TargetOptions: optionsString, Project: project1, } - _, err := sampleProvider.DestroyProject(projectReq) + _, err := flyProvider.DestroyProject(projectReq) if err != nil { - t.Errorf("Error deleting project: %s", err) + t.Fatalf("Error destroying project: %s", err) } + time.Sleep(3 * time.Second) - TestDestroyWorkspace(t) + _, err = flyutil.GetMachine(project1, targetOptions) + if err == nil { + t.Fatalf("Error destroyed project still exists") + } } func init() { - _, err := sampleProvider.Initialize(daytona_provider.InitializeProviderRequest{ + _, err := flyProvider.Initialize(provider.InitializeProviderRequest{ BasePath: "/tmp/workspaces", ServerDownloadUrl: "https://download.daytona.io/daytona/install.sh", ServerVersion: "latest", @@ -137,6 +122,5 @@ func init() { if err != nil { panic(err) } - optionsString = string(opts) } diff --git a/pkg/provider/util/fly.go b/pkg/provider/util/fly.go new file mode 100644 index 0000000..fcfa122 --- /dev/null +++ b/pkg/provider/util/fly.go @@ -0,0 +1,269 @@ +package util + +import ( + "context" + "fmt" + "io" + "net/http" + "slices" + "time" + + "github.com/daytonaio/daytona-provider-fly/internal" + "github.com/daytonaio/daytona-provider-fly/pkg/types" + "github.com/daytonaio/daytona/pkg/workspace" + log "github.com/sirupsen/logrus" + "github.com/superfly/fly-go" + "github.com/superfly/fly-go/flaps" + "github.com/superfly/fly-go/tokens" +) + +// CreateMachine creates a new machine for the provided workspace. +func CreateMachine(project *workspace.Project, opts *types.TargetOptions, initScript string) (*fly.Machine, error) { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return nil, err + } + + err = flapsClient.CreateApp(context.Background(), machineName, opts.OrgSlug) + if err != nil { + return nil, err + } + + err = flapsClient.WaitForApp(context.Background(), machineName) + if err != nil { + return nil, err + } + + volume, err := flapsClient.CreateVolume(context.Background(), fly.CreateVolumeRequest{ + Name: getVolumeName(project.WorkspaceId), + SizeGb: &opts.DiskSize, + Region: opts.Region, + }) + if err != nil { + return nil, err + } + + envVars := map[string]string{} + for key, value := range project.EnvVars { + envVars[key] = value + } + + return flapsClient.Launch(context.Background(), fly.LaunchMachineInput{ + Name: machineName, + Config: &fly.MachineConfig{ + VMSize: opts.Size, + Image: project.Image, + Mounts: []fly.MachineMount{ + { + Name: volume.Name, + Volume: volume.ID, + Path: fmt.Sprintf("/home/%s", project.User), + SizeGb: opts.DiskSize, + }, + }, + Env: envVars, + Init: fly.MachineInit{ + Entrypoint: []string{"bash", "-c", initScript}, + }, + }, + Region: opts.Region, + }) +} + +// IsMachineReady checks if the machine for the given workspace is ready to use. +// It returns true if the machine is in a known state (created, started, or stopped), +// otherwise it returns false. +func IsMachineReady(project *workspace.Project, opts *types.TargetOptions) bool { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return false + } + + machine, err := findMachine(flapsClient, machineName) + if err != nil { + return false + } + + knownStatus := []string{fly.MachineStateCreated, fly.MachineStateStarted, fly.MachineStateStopped} + return slices.Contains(knownStatus, machine.State) +} + +// StartMachine starts the machine for the provided workspace. +func StartMachine(project *workspace.Project, opts *types.TargetOptions) error { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return err + } + + machine, err := findMachine(flapsClient, machineName) + if err != nil { + return err + } + + // Start the machine if it is stopped + if machine.State == fly.MachineStateStopped { + _, err = flapsClient.Start(context.Background(), machine.ID, "") + if err != nil { + return err + } + } + + return nil +} + +// StopMachine stops the machine for the provided workspace. +func StopMachine(project *workspace.Project, opts *types.TargetOptions) error { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return err + } + + machine, err := findMachine(flapsClient, machineName) + if err != nil { + return err + } + + return flapsClient.Stop(context.Background(), fly.StopMachineInput{ID: machine.ID}, "") +} + +// DeleteMachine deletes the machine for the provided workspace. +func DeleteMachine(project *workspace.Project, opts *types.TargetOptions) error { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return err + } + + // TODO: use delete method from flaps client when implemented in sdk + path := fmt.Sprintf("/apps/%s", machineName) + req, err := flapsClient.NewRequest(context.Background(), http.MethodDelete, path, nil, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("unexpected error while deleting the app status code: %d", resp.StatusCode) + } + + return nil +} + +// GetMachine returns the machine for the provided workspace. +func GetMachine(project *workspace.Project, opts *types.TargetOptions) (*fly.Machine, error) { + machineName := getMachineName(project.WorkspaceId) + flapsClient, err := createFlapsClient(machineName, *opts.AuthToken) + if err != nil { + return nil, err + } + + return findMachine(flapsClient, machineName) +} + +// GetMachineLogs fetches app logs for a specified machine and writes the fetched log entries to the logger. +func GetMachineLogs(project *workspace.Project, opts *types.TargetOptions, machineId string, logger io.Writer) error { + machineName := getMachineName(project.WorkspaceId) + + fly.SetBaseURL("https://api.fly.io") + client := fly.NewClientFromOptions(fly.ClientOptions{ + Tokens: tokens.Parse(*opts.AuthToken), + Name: machineName, + Version: internal.Version, + }) + + outLog := make(chan string) + go func() { + for entry := range outLog { + logger.Write([]byte(entry)) + } + }() + + return pollLogs(outLog, client, machineName, opts.Region, machineId) +} + +// createFlapsClient creates a new flaps client. +func createFlapsClient(machineName string, accessToken string) (*flaps.Client, error) { + return flaps.NewWithOptions(context.Background(), flaps.NewClientOpts{ + AppName: machineName, + Tokens: tokens.Parse(accessToken), + Logger: log.New(), + }) +} + +// findMachine finds the machine with the provided name. +func findMachine(flapsClient *flaps.Client, machineName string) (*fly.Machine, error) { + machineList, err := flapsClient.List(context.Background(), "") + if err != nil { + return nil, err + } + + for _, m := range machineList { + if m.Name == machineName { + return m, nil + } + } + + return nil, fmt.Errorf("machine %s not found", machineName) +} + +// getMachineName generates a machine name for the provided workspace. +func getMachineName(workspaceId string) string { + return fmt.Sprintf("daytona-%s", workspaceId) +} + +// getVolumeName generates a volume name for the provided workspace. +func getVolumeName(workspaceId string) string { + name := fmt.Sprintf("daytona_%s", workspaceId) + if len(name) > 30 { + return name[:30] + } + return name +} + +// pollLogs fetches app logs for a specified app name, region, and machine ID using the provided fly.Client. +// It sends the fetched log entries to the out channel. +// It continues fetching logs indefinitely until an error occurs. +func pollLogs(out chan<- string, client *fly.Client, appName, region, machineId string) error { + var ( + prevToken string + nextToken string + ) + + for { + entries, token, err := client.GetAppLogs(context.Background(), appName, nextToken, region, machineId) + if err != nil { + return err + } + + // Adds a delay in fetching logs when current log entries have been fully fetched. + // This is done to reduce pressure on the server and give time for new logs to accumulate. + if token == prevToken { + time.Sleep(10 * time.Second) + } + + prevToken = token + if token != "" { + nextToken = token + } + + for _, entry := range entries { + logMessage := fmt.Sprintf("%s app[%s] %s [%s] %s \n", + entry.Timestamp, + entry.Instance, + entry.Region, + entry.Level, + entry.Message, + ) + out <- logMessage + } + } +} diff --git a/pkg/types/metadata.go b/pkg/types/metadata.go index 77538eb..da10f3d 100644 --- a/pkg/types/metadata.go +++ b/pkg/types/metadata.go @@ -1,9 +1,6 @@ package types -type WorkspaceMetadata struct { - Property string -} - type ProjectMetadata struct { - Property string + MachineId string + VolumeId string } diff --git a/pkg/types/targets.go b/pkg/types/targets.go index bee4e03..688c6c5 100644 --- a/pkg/types/targets.go +++ b/pkg/types/targets.go @@ -2,38 +2,50 @@ package types import ( "encoding/json" + "fmt" + "os" "github.com/daytonaio/daytona/pkg/provider" ) type TargetOptions struct { - RequiredString string `json:"Required String"` - OptionalString *string `json:"Optional String,omitempty"` - OptionalInt *int `json:"Optional Int,omitempty"` - FilePath *string `json:"File Path"` + Region string `json:"Region"` + Size string `json:"Size"` + DiskSize int `json:"Disk Size"` + OrgSlug string `json:"Org Slug"` + AuthToken *string `json:"Auth Token,omitempty"` } func GetTargetManifest() *provider.ProviderTargetManifest { return &provider.ProviderTargetManifest{ - "Required String": provider.ProviderTargetProperty{ + "Region": provider.ProviderTargetProperty{ + Type: provider.ProviderTargetPropertyTypeString, + Description: "The region where the fly machine resides. If not specified, near region will be used.", + }, + "Size": provider.ProviderTargetProperty{ Type: provider.ProviderTargetPropertyTypeString, - DefaultValue: "default-required-string", + DefaultValue: "shared-cpu-4x", + Description: "The size of the fly machine. Default is shared-cpu-4x. List of available sizes " + + "https://fly.io/docs/about/pricing/#started-fly-machines", }, - "Optional String": provider.ProviderTargetProperty{ - Type: provider.ProviderTargetPropertyTypeString, - InputMasked: true, + "Disk Size": provider.ProviderTargetProperty{ + Type: provider.ProviderTargetPropertyTypeInt, + DefaultValue: "10", + Description: "The size of the disk in GB.", }, - "Optional Int": provider.ProviderTargetProperty{ - Type: provider.ProviderTargetPropertyTypeInt, + "Org Slug": provider.ProviderTargetProperty{ + Type: provider.ProviderTargetPropertyTypeString, + Description: "The organization name to create the fly machine in.", }, - "File Path": provider.ProviderTargetProperty{ - Type: provider.ProviderTargetPropertyTypeFilePath, - DefaultValue: "~/.ssh", - DisabledPredicate: "^default-target$", + "Auth Token": provider.ProviderTargetProperty{ + Type: provider.ProviderTargetPropertyTypeString, + InputMasked: true, + Description: "If empty, token will be fetched from the FLY_ACCESS_TOKEN environment variable.", }, } } +// ParseTargetOptions parses the target options from the JSON string. func ParseTargetOptions(optionsJson string) (*TargetOptions, error) { var targetOptions TargetOptions err := json.Unmarshal([]byte(optionsJson), &targetOptions) @@ -41,5 +53,21 @@ func ParseTargetOptions(optionsJson string) (*TargetOptions, error) { return nil, err } + if targetOptions.AuthToken == nil { + // Fetch token from environment variable + token, ok := os.LookupEnv("FLY_ACCESS_TOKEN") + if ok { + targetOptions.AuthToken = &token + } + } + + if targetOptions.AuthToken == nil { + return nil, fmt.Errorf("auth token not set in env/target options") + } + + if targetOptions.OrgSlug == "" { + return nil, fmt.Errorf("org slug not set in target options") + } + return &targetOptions, nil } diff --git a/pkg/types/targets_test.go b/pkg/types/targets_test.go new file mode 100644 index 0000000..af6f83f --- /dev/null +++ b/pkg/types/targets_test.go @@ -0,0 +1,74 @@ +package types + +import ( + "testing" +) + +func TestGetTargetManifest(t *testing.T) { + targetManifest := GetTargetManifest() + if targetManifest == nil { + t.Fatalf("Expected target manifest but got nil") + } + + fields := [5]string{"Region", "Size", "Disk Size", "Org Slug", "Auth Token"} + for _, field := range fields { + if _, ok := (*targetManifest)[field]; !ok { + t.Errorf("Expected field %s in target manifest but it was not found", field) + } + } +} + +func TestParseTargetOptions(t *testing.T) { + cases := []struct { + name string + jsonInput string + setAccessTokenEnv bool + isValid bool + }{ + { + name: "Minimal valid input", + jsonInput: `{"Org Slug":"org","Auth Token":"token"}`, + setAccessTokenEnv: false, + isValid: true, + }, + { + name: "Missing auth token, but present in env", + jsonInput: `{"Org Slug":"org"}`, + setAccessTokenEnv: true, + isValid: true, + }, + { + name: "Missing auth token", + jsonInput: `{"Org Slug":"org"}`, + setAccessTokenEnv: false, + isValid: false, + }, + { + name: "Missing org slug", + jsonInput: `{"Auth Token":"token"}`, + setAccessTokenEnv: false, + isValid: false, + }, + { + name: "Empty input", + jsonInput: `{}`, + setAccessTokenEnv: false, + isValid: false, + }, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + if testCase.setAccessTokenEnv { + t.Setenv("FLY_ACCESS_TOKEN", "token") + } + + _, err := ParseTargetOptions(testCase.jsonInput) + if testCase.isValid && err != nil { + t.Errorf("Expected valid target options but got error: %s", err) + } else if !testCase.isValid && err == nil { + t.Errorf("Expected error for invalid target options but got none") + } + }) + } +}