diff --git a/go.mod b/go.mod index fedff089..06d2b313 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/didi/gendry v1.7.0 github.com/djherbis/times v1.5.0 github.com/evanphx/json-patch v4.12.0+incompatible - github.com/go-git/go-git/v5 v5.11.0 github.com/go-sql-driver/mysql v1.6.0 github.com/go-test/deep v1.0.3 github.com/goccy/go-yaml v1.11.0 @@ -90,6 +89,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/deckarep/golang-set v1.7.1 // indirect github.com/go-errors/errors v1.4.2 // indirect + github.com/go-git/go-git/v5 v5.11.0 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect @@ -129,7 +129,6 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/aliyun/aliyun-secretsmanager-client-go v1.1.4 github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -146,12 +145,10 @@ require ( github.com/aws/smithy-go v1.17.0 // indirect github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/jsonv v1.1.3 // indirect github.com/chai2010/protorpc v1.1.4 // indirect - github.com/cheggaaa/pb v1.0.29 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.3 // indirect github.com/containerd/containerd v1.7.5 // indirect @@ -177,7 +174,6 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gofrs/flock v0.8.1 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect @@ -235,8 +231,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc4 // indirect - github.com/opentracing/basictracer-go v1.1.0 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/otiai10/copy v1.12.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -249,8 +243,6 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect @@ -260,9 +252,6 @@ require ( github.com/thoas/go-funk v0.9.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect - github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect - github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -291,11 +280,9 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/klog/v2 v2.100.1 k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - lukechampine.com/frand v1.4.2 // indirect oras.land/oras-go v1.2.3 // indirect oras.land/oras-go/v2 v2.3.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect - sourcegraph.com/sourcegraph/appdash v0.0.0-20211028080628-e2786a622600 // indirect ) diff --git a/go.sum b/go.sum index 070ce0fd..5078bda6 100644 --- a/go.sum +++ b/go.sum @@ -47,7 +47,6 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.4.0 h1:yxQ63CFIA8Sxkh0vqIofuNrsXl/LZ42TpeTLV4Nb5HM= github.com/DATA-DOG/go-sqlmock v1.4.0/go.mod h1:3TucWNLPFOLcHhha1CPp7Kis1UG2h/AqGROPyOeZzsM= -github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= @@ -69,8 +68,6 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjA github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -151,8 +148,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= @@ -174,8 +169,6 @@ github.com/chai2010/jsonv v1.1.3 h1:gBIHXn/5mdEPTuWZfjC54fn/yUSRR8OGobXobcc6now= github.com/chai2010/jsonv v1.1.3/go.mod h1:mEoT1dQ9qVF4oP9peVTl0UymTmJwXoTDOh+sNA6+XII= github.com/chai2010/protorpc v1.1.4 h1:CTtFUhzXRoeuR7FtgQ2b2vdT/KgWVpCM+sIus8zJjHs= github.com/chai2010/protorpc v1.1.4/go.mod h1:/wO0kiyVdu7ug8dCMrA2yDr2vLfyhsLEuzLa9J2HJ+I= -github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= -github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -239,7 +232,6 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= @@ -288,10 +280,7 @@ github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= @@ -454,7 +443,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= @@ -490,7 +478,6 @@ github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zg github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -501,7 +488,6 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -572,11 +558,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= -github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= @@ -637,10 +618,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -692,12 +669,6 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= -github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= -github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= -github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= -github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -762,8 +733,6 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -781,7 +750,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -819,7 +787,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -882,14 +849,12 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -995,8 +960,6 @@ kcl-lang.io/lib v0.7.3 h1:YsKBo5jrICQ3fJobywkB9dFfVIW/ixCAkqmYQ5Z2lqQ= kcl-lang.io/lib v0.7.3/go.mod h1:ubsalGXxJaa5II/EsHmsI/tL2EluYHIcW+BwzQPt+uY= kusionstack.io/kube-api v0.1.1 h1:ieoZhaUfK78hsyQ7GsU6ZuxBAcVU+ZuKs7vedGkO8sI= kusionstack.io/kube-api v0.1.1/go.mod h1:QIQrH+MK9xuV+mXCAkk6DN8z6b8oyf4XN0VRccmHH/k= -lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= -lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= oras.land/oras-go v1.2.3 h1:v8PJl+gEAntI1pJ/LCrDgsuk+1PKVavVEPsYIHFE5uY= oras.land/oras-go v1.2.3/go.mod h1:M/uaPdYklze0Vf3AakfarnpoEckvw0ESbRdN8Z1vdJg= oras.land/oras-go/v2 v2.3.0 h1:lqX1aXdN+DAmDTKjiDyvq85cIaI4RkIKp/PghWlAGIU= @@ -1014,5 +977,3 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kF sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= -sourcegraph.com/sourcegraph/appdash v0.0.0-20211028080628-e2786a622600 h1:hfyJ5ku9yFtLVOiSxa3IN+dx5eBQT9mPmKFypAmg8XM= -sourcegraph.com/sourcegraph/appdash v0.0.0-20211028080628-e2786a622600/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index a1d03d00..71276c45 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -91,7 +91,7 @@ func NewKusionctlCmd(o KusionctlOptions) *cobra.Command { Message: "Configuration Commands:", Commands: []*cobra.Command{ workspace.NewCmd(), - cmdinit.NewCmdInit(), + cmdinit.NewCmd(), build.NewCmdBuild(), }, }, diff --git a/pkg/cmd/init/init.go b/pkg/cmd/init/init.go index 2914a84f..123abbd0 100644 --- a/pkg/cmd/init/init.go +++ b/pkg/cmd/init/init.go @@ -3,112 +3,41 @@ package init import ( "github.com/spf13/cobra" "k8s.io/kubectl/pkg/util/templates" - "kusionstack.io/kusion/pkg/cmd/util" "kusionstack.io/kusion/pkg/util/i18n" ) -func NewCmdInit() *cobra.Command { +func NewCmd() *cobra.Command { var ( - initShort = i18n.T(`Initialize the scaffolding for a project`) - - initLong = i18n.T(` - This command initializes the scaffolding for a project, generating a project from an appointed template with correct structure. - - The scaffold templates can be retrieved from local or online. The built-in templates are used by default, self-defined templates are also supported by assigning the template repository path.`) + short = i18n.T(`Initialize the scaffolding for a demo project`) - initExample = i18n.T(` - # Initialize a project from internal templates - kusion init + long = i18n.T(` + This command initializes the scaffolding for a demo project with the name of the current directory to help users quickly get started. + + Note that current directory needs to be an empty directory.`) - # Initialize a project from default online templates - kusion init --online=true - - # Initialize a project from a specific online template - kusion init https://github.com// --online=true - - # Initialize a project from a specific local template - kusion init /path/to/templates`) + example = i18n.T(` + # Initialize a demo project with the name of the current directory + mkdir quickstart && cd quickstart + kusion init`) ) - o := NewInitOptions() + o := NewOptions() cmd := &cobra.Command{ - Use: "init", - Short: initShort, - Long: templates.LongDesc(initLong), - Example: templates.Examples(initExample), - DisableFlagsInUseLine: true, + Use: "init", + Short: short, + Long: templates.LongDesc(long), + Example: templates.Examples(example), + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) (err error) { defer util.RecoverErr(&err) util.CheckErr(o.Complete(args)) util.CheckErr(o.Validate()) util.CheckErr(o.Run()) - return - }, - } - - cmd.Flags().StringVar( - &o.TemplateName, "template-name", "", - i18n.T("Initialize with specified template. If not specified, a prompt will request it")) - cmd.Flags().StringVar( - &o.ProjectName, "project-name", "", - i18n.T("Initialize with specified project name. If not specified, a prompt will request it")) - cmd.Flags().BoolVar( - &o.Force, "force", false, - i18n.T("Force generating the scaffolding files, even if it would change the existing files")) - cmd.PersistentFlags().BoolVar( - &o.Online, "online", false, - i18n.T("Use templates from online repository to initialize project, or use locally cached templates")) - cmd.Flags().BoolVar( - &o.Yes, "yes", false, - i18n.T("Skip prompts and proceed with default values")) - cmd.Flags().StringVar( - &o.CustomParamsJSON, "custom-params", "", - i18n.T("Custom params in JSON. If specified, it will be used as the template default value and skip prompts")) - - templatesCmd := newCmdTemplates() - cmd.AddCommand(templatesCmd) - return cmd -} -func newCmdTemplates() *cobra.Command { - var ( - templatesShort = i18n.T(`List templates used to initialize a project`) - - templatesLong = i18n.T(` - This command gets the descriptions and definitions of the templates which are used to initialize the project scaffolding.`) - - templatesExample = i18n.T(` - # Get name and description of internal templates - kusion init templates - - # Get templates from specific templates repository - kusion init templates https://github.com// --online=true`) - ) - - o := NewTemplatesOptions() - cmd := &cobra.Command{ - Use: "templates", - Short: templatesShort, - Long: templates.LongDesc(templatesLong), - Example: templates.Examples(templatesExample), - DisableFlagsInUseLine: true, - RunE: func(cmd *cobra.Command, args []string) (err error) { - defer util.RecoverErr(&err) - online, err := cmd.InheritedFlags().GetBool("online") - if err != nil { - return err - } - util.CheckErr(o.Complete(args, online)) - util.CheckErr(o.Validate()) - util.CheckErr(o.Run()) return }, } - cmd.Flags().StringVarP( - &o.Output, "output", "o", "", - i18n.T("Specify the output format of templates. If specified, only support json for now; if not, template name and description is given")) - return cmd } diff --git a/pkg/cmd/init/init_test.go b/pkg/cmd/init/init_test.go index c2dd7c78..08d6046c 100644 --- a/pkg/cmd/init/init_test.go +++ b/pkg/cmd/init/init_test.go @@ -1,25 +1,22 @@ package init import ( - "os" "testing" "github.com/bytedance/mockey" "github.com/stretchr/testify/assert" ) -func Test_CmdInit(t *testing.T) { - mockey.PatchConvey("cmd init", t, func() { - // patch human interact - patchChooseTemplate() - patchPromptValue() +func TestNewCmd(t *testing.T) { + t.Run("successfully initiate a demo project", func(t *testing.T) { + mockey.PatchConvey("mock complete, validate and run", t, func() { + mockey.Mock((*Options).Complete).Return(nil).Build() + mockey.Mock((*Options).Validate).Return(nil).Build() + mockey.Mock((*Options).Run).Return(nil).Build() - cmd := NewCmdInit() - _ = cmd.Flags().Set("project-name", "test") - // clean data - defer os.RemoveAll("test") - - err := cmd.Execute() - assert.Nil(t, err) + cmd := NewCmd() + err := cmd.Execute() + assert.Nil(t, err) + }) }) } diff --git a/pkg/cmd/init/options.go b/pkg/cmd/init/options.go index 71e9fa20..0e63abcc 100644 --- a/pkg/cmd/init/options.go +++ b/pkg/cmd/init/options.go @@ -1,443 +1,58 @@ package init import ( - "encoding/json" "errors" "fmt" - "os" - "path/filepath" - "sort" - "github.com/AlecAivazis/survey/v2" - "github.com/pterm/pterm" - - "kusionstack.io/kusion/pkg/log" + "kusionstack.io/kusion/pkg/cmd/init/util" "kusionstack.io/kusion/pkg/scaffold" ) -const jsonOutput = "json" +var ErrNotEmptyArgs = errors.New("no args accepted") type Options struct { - TemplateRepoPath string - TemplateName string - Online bool - ProjectName string - Force bool - Yes bool - CustomParamsJSON string + Name string + ProjectDir string } -func NewInitOptions() *Options { +func NewOptions() *Options { return &Options{} } func (o *Options) Complete(args []string) error { - if o.Online { // use online templates, official link or user-specified link - o.TemplateRepoPath = onlineTemplateRepoPath(args) - } else { // use offline templates, internal templates or user-specified local dir - path, err := localTemplateRepoPath(args) - if err != nil { - return err - } - o.TemplateRepoPath = path - } - return nil -} - -func (o *Options) Validate() error { - if o.Online { - return nil - } - // offline mode may need to generate templates - if err := validateLocalTemplateRepoPath(o.TemplateRepoPath); err != nil { - return err - } - return nil -} - -func (o *Options) Run() error { - // Retrieve the template repo. - repo, err := retrieveTemplateRepo(o.TemplateRepoPath, o.Online) - if err != nil { - return err - } - defer deleteTemplateRepo(repo) - - // List the templates from the repo. - templates, err := getTemplates(repo) - if err != nil { - return err - } - - // Choose template - var template scaffold.Template - if o.TemplateName == "" { - // if template name is not specified, choose template by prompt - if template, err = chooseTemplate(templates); err != nil { - return err - } - } else { - // if template name is specified, find template from repo data - var templateExist bool - for _, t := range templates { - if t.Name == o.TemplateName { - templateExist = true - template = t - break - } - } - if !templateExist { - return fmt.Errorf("template %s does not exist", o.TemplateName) - } - } - - // Parse customParams if not empty - var tc scaffold.TemplateConfig - if o.CustomParamsJSON != "" { - if err := json.Unmarshal([]byte(o.CustomParamsJSON), &tc); err != nil { - return err - } - if tc.ProjectName == "" { - return errors.New("`ProjectName` is missing in custom params") - } - - o.ProjectName = tc.ProjectName - pterm.Bold.Println("Use custom params to render template...") - } - - // Show instructions, if we're going to use interactive mode - if !o.Yes && o.CustomParamsJSON == "" { - pterm.Println("This command will walk you through creating a new kusion project.") - pterm.Println() - pterm.Printfln("Enter a value or leave blank to accept the (default), and press %s.", - pterm.Cyan("")) - pterm.Printfln("Press %s at any time to quit.", pterm.Cyan("^C")) - pterm.Println() - pterm.Bold.Println("Project Config:") - } - - // o.ProjectName is used to make root directory - if o.ProjectName != "" { - if err := scaffold.ValidateProjectName(o.ProjectName); err != nil { - return fmt.Errorf("'%s' is not a valid project name as [%v]", o.ProjectName, err) - } - } else { - defaultName := template.ProjectName - if defaultName == "" { - defaultName = template.Name - } - if !o.Yes { - o.ProjectName, err = promptValue("ProjectName", "ProjectName is a required fully qualified name", defaultName, scaffold.ValidateProjectName) - if err != nil { - return err - } - } else { - o.ProjectName = defaultName - } - } - - if o.CustomParamsJSON == "" { - // Prompt project configs from kusion.yaml - tc.ProjectConfig = make(map[string]interface{}) - for _, f := range template.ProjectFields { - tc.ProjectConfig[f.Name] = f.Default - // We don't prompt non-primitive types, such as: array and struct - if !f.Type.IsPrimitive() || o.Yes { - continue - } - // Prompt and restore actual value - actual, err := promptAndRestore(f) - if err != nil { - return err - } - tc.ProjectConfig[f.Name] = actual - } - - // Prompt stack configs from kusion.yaml - tc.StacksConfig = make(map[string]map[string]interface{}) - for _, stack := range template.StackTemplates { - if !o.Yes { - pterm.Bold.Printfln("Stack Config: %s", pterm.Cyan(stack.Name)) - } - configs := make(map[string]interface{}) - for _, f := range stack.Fields { - configs[f.Name] = f.Default - // We don't prompt non-primitive types, such as: array and struct - if !f.Type.IsPrimitive() || o.Yes { - continue - } - // Prompt and restore actual value - actual, err := promptAndRestore(f) - if err != nil { - return err - } - configs[f.Name] = actual - } - tc.StacksConfig[stack.Name] = configs - } + if len(args) > 0 { + return ErrNotEmptyArgs } - // Get the current working directory. - cwd, err := os.Getwd() + dir, name, err := util.GetDirAndName() if err != nil { - return fmt.Errorf("getting the working directory: %w", err) - } - // Make dest directory with project name - desDir := filepath.Join(cwd, o.ProjectName) - - // reuse the project name - if tc.ProjectConfig["ProjectName"] == nil { - tc.ProjectConfig["ProjectName"] = o.ProjectName - } - // Actually copy the files. - if err = scaffold.RenderLocalTemplate(template.Dir, desDir, o.Force, &tc); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("template '%s' not found: %w", template.Name, err) - } return err } - fmt.Printf("Created project '%s'\n", o.ProjectName) - - return nil -} - -type TemplatesOptions struct { - TemplateRepoPath string - Online bool - Output string -} - -func NewTemplatesOptions() *TemplatesOptions { - return &TemplatesOptions{} -} + o.ProjectDir = dir + o.Name = name -func (o *TemplatesOptions) Complete(args []string, online bool) error { - o.Online = online - if o.Online { - o.TemplateRepoPath = onlineTemplateRepoPath(args) - } else { - if path, err := localTemplateRepoPath(args); err != nil { - return err - } else { - o.TemplateRepoPath = path - } - } return nil } -func (o *TemplatesOptions) Validate() error { - if o.Output != "" && o.Output != jsonOutput { - return errors.New("invalid output type, supported types: json") - } - if !o.Online { - if err := validateLocalTemplateRepoPath(o.TemplateRepoPath); err != nil { - return err - } - } - return nil -} - -func (o *TemplatesOptions) Run() error { - // retrieve template repo - repo, err := retrieveTemplateRepo(o.TemplateRepoPath, o.Online) - if err != nil { +func (o *Options) Validate() error { + if err := util.ValidateProjectDir(o.ProjectDir); err != nil { return err } - defer deleteTemplateRepo(repo) - // get templates from repo, and print it - templates, err := getTemplates(repo) - if err != nil { - return err - } - templateOutputs, err := fmtTemplatesOutput(templates, o.Output == jsonOutput) - if err != nil { + if err := util.ValidateProjectName(o.Name); err != nil { return err } - for _, output := range templateOutputs { - pterm.Println(output) - } - return nil -} - -// onlineTemplateRepoPath parses url from args, called when --online is true. -func onlineTemplateRepoPath(args []string) string { - if len(args) > 0 { - // user-specified link - return args[0] - } - return "" // use official link -} -// localTemplateRepoPath parses path from args, if not specified, use default InternalTemplateDir, -// called when --online is false. -func localTemplateRepoPath(args []string) (string, error) { - if len(args) > 0 { - // user-specified local dir - return args[0], nil - } else { - // use internal templates - internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) - if err != nil { - return "", err - } - return internalTemplateDir, nil - } -} - -// validateLocalTemplateRepoPath checks the path is valid or not. -func validateLocalTemplateRepoPath(path string) error { - // offline mode may need to generate templates - internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) - if err != nil { - return err - } - // gen internal templates first before using it - if internalTemplateDir == path { - if _, err = os.Stat(path); os.IsNotExist(err) { - return scaffold.GenInternalTemplates() - } - } return nil } -// retrieveTemplateRepo gets template repos from online or local, with specified url or path. -func retrieveTemplateRepo(templateRepoPath string, online bool) (scaffold.TemplateRepository, error) { - return scaffold.RetrieveTemplates(templateRepoPath, online) -} - -// deleteTemplateRepo is used to delete the files of the template repos, log warn if failed. -func deleteTemplateRepo(repo scaffold.TemplateRepository) { - if err := repo.Delete(); err != nil { - log.Warnf("Explicitly ignoring and discarding error: %w", err) - } -} - -// getTemplates get templates from template repo. -func getTemplates(repo scaffold.TemplateRepository) ([]scaffold.Template, error) { - // List the templates from the repo. - templates, err := repo.Templates() - if err != nil { - return nil, err - } - if len(templates) == 0 { - return nil, errors.New("no templates") - } - return templates, nil -} - -// fmtTemplatesOutput is used to format the templates output, in text or json. -func fmtTemplatesOutput(templates []scaffold.Template, jsonFmt bool) ([]string, error) { - var outputs []string - if jsonFmt { - output, err := json.Marshal(templates) - if err != nil { - return nil, fmt.Errorf("failed to json marshal templates as %w", err) - } - outputs = append(outputs, string(output)) - } else { - // Find the longest name length. Used to add padding between the name and description. - maxNameLength := 0 - for _, template := range templates { - if len(template.Name) > maxNameLength { - maxNameLength = len(template.Name) - } - } - // Create the option string that combines the name, padding, and description. - for _, template := range templates { - output := fmt.Sprintf(fmt.Sprintf("%%%ds %%s", -maxNameLength), template.Name, template.Description) - outputs = append(outputs, output) - } - } - - return outputs, nil -} - -// chooseTemplate will prompt the user to choose amongst the available templates. -func chooseTemplate(templates []scaffold.Template) (scaffold.Template, error) { - const chooseTemplateErr = "no template selected; please use `kusion init` to choose one" - - options, optionToTemplateMap := templatesToOptionArrayAndMap(templates) - message := pterm.Cyan("Please choose a template:") - prompt := &survey.Select{ - Message: message, - PageSize: 10, - Options: options, - } - - var selectedOption scaffold.Template - var has bool - var option string - - for { - err := survey.AskOne(prompt, &option) - if err != nil { - return scaffold.Template{}, errors.New(chooseTemplateErr) - } - selectedOption, has = optionToTemplateMap[option] - if has { - break - } - } - - return selectedOption, nil -} - -// templatesToOptionArrayAndMap returns an array of option strings and a map of option strings to templates. -// Each option string is made up of the template name and description with some padding in between. -func templatesToOptionArrayAndMap(templates []scaffold.Template) ([]string, map[string]scaffold.Template) { - // Build the array and map. - options, _ := fmtTemplatesOutput(templates, false) - nameToTemplateMap := make(map[string]scaffold.Template) - for i, template := range templates { - nameToTemplateMap[options[i]] = template - } - sort.Strings(options) - - return options, nameToTemplateMap -} - -// promptAndRestore will prompt f.Value first and restore its actual value based on f.Type -func promptAndRestore(f *scaffold.FieldTemplate) (interface{}, error) { - // Prompt always return string value, must restore f type - input, err := promptValue(f.Name, f.Description, fmt.Sprintf("%v", f.Default), nil) - if err != nil { - return nil, err - } - // Restore f type - actual, err := f.RestoreActualValue(input) - if err != nil { - return nil, err - } - return actual, nil -} - -func promptValue(valueType string, description string, defaultValue string, isValidFn func(value string) error) (value string, err error) { - prompt := &survey.Input{ - Message: fmt.Sprintf("%s:", valueType), - Default: defaultValue, - Help: description, +func (o *Options) Run() error { + if err := scaffold.GenDemoProject(o.ProjectDir, o.Name); err != nil { + return err } - for { - // you can pass multiple validators here and survey will make sure each one passes - err = survey.AskOne(prompt, &value) - if err != nil { - return "", err - } + fmt.Printf("Initiated demo project '%s' successfully\n", o.Name) - // ensure user input is valid - if isValidFn != nil { - if validationError := isValidFn(value); validationError != nil { - // If validation failed, let the user know. If interactive, we will print the error and - // prompt the user again - fmt.Printf("Sorry, '%s' is not a valid %s. %s.\n", value, valueType, validationError) - continue - } - } - - break - } - return value, nil + return nil } diff --git a/pkg/cmd/init/options_test.go b/pkg/cmd/init/options_test.go index c8db4ef5..1b8aa0b0 100644 --- a/pkg/cmd/init/options_test.go +++ b/pkg/cmd/init/options_test.go @@ -1,154 +1,108 @@ package init import ( - "fmt" - "os" - "reflect" + "errors" "testing" - "github.com/AlecAivazis/survey/v2" "github.com/bytedance/mockey" "github.com/stretchr/testify/assert" - + "kusionstack.io/kusion/pkg/cmd/init/util" "kusionstack.io/kusion/pkg/scaffold" ) -func patchChooseTemplate() { - mockey.Mock(chooseTemplate).To(func(templates []scaffold.Template) (scaffold.Template, error) { - return templates[0], nil - }).Build() -} +func TestOptions_Complete(t *testing.T) { + t.Run("not empty args", func(t *testing.T) { + opts := NewOptions() + args := []string{"quickstart"} -func patchPromptValue() { - mockey.Mock(promptValue).To(func(valueType, description, defaultValue string, isValidFn func(value string) error) (string, error) { - return defaultValue, nil - }).Build() -} + err := opts.Complete(args) + assert.ErrorContains(t, err, ErrNotEmptyArgs.Error()) + }) + + t.Run("failed to get directory and name", func(t *testing.T) { + mockey.PatchConvey("mock util.GetDirAndName", t, func() { + mockey.Mock(util.GetDirAndName).To(func() (dir, name string, err error) { + return "", "", errors.New("failed to get directory and name") + }).Build() + + opts := NewOptions() + args := []string{} -func TestRun(t *testing.T) { - mockey.PatchConvey("init from official url", t, func() { - patchChooseTemplate() - patchPromptValue() - - o := &Options{ - Force: true, - } - err := o.Complete(nil) - assert.Nil(t, err) - err = o.Run() - assert.Nil(t, err) - _ = os.RemoveAll(o.ProjectName) + err := opts.Complete(args) + assert.ErrorContains(t, err, "failed to get directory and name") + }) }) - mockey.PatchConvey("init templates from official url", t, func() { - o := &TemplatesOptions{ - Output: jsonOutput, - } - err := o.Complete(nil, true) - assert.Nil(t, err) - err = o.Run() - assert.Nil(t, err) + t.Run("successfully complete the options", func(t *testing.T) { + mockey.PatchConvey("mock util.GetDirAndName", t, func() { + mockey.Mock(util.GetDirAndName).To(func() (dir, name string, err error) { + return "/dir/to/quickstart", "quickstart", nil + }).Build() + + opts := NewOptions() + args := []string{} + + err := opts.Complete(args) + assert.Nil(t, err) + }) }) } -func TestChooseTemplate(t *testing.T) { - mockey.PatchConvey("choose first", t, func() { - // survey.AskOne - mockey.Mock( - survey.AskOne).To( - func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error { - reflect.ValueOf(response).Elem().Set(reflect.ValueOf("foo1 ")) - return nil - }, - ).Build() - - // test data - templates := []scaffold.Template{ - {Name: "foo1", ProjectTemplate: &scaffold.ProjectTemplate{}}, - {Name: "foo2", ProjectTemplate: &scaffold.ProjectTemplate{}}, - } - chosen, err := chooseTemplate(templates) - if err != nil { - return - } - assert.Nil(t, err) - assert.Equal(t, templates[0], chosen) +func TestOptions_Validate(t *testing.T) { + t.Run("failed to validate project directory", func(t *testing.T) { + mockey.PatchConvey("mock util.ValidateProjectDir", t, func() { + mockey.Mock(util.ValidateProjectDir). + Return(errors.New("failed to validate project directory")).Build() + + opts := NewOptions() + err := opts.Validate() + assert.ErrorContains(t, err, "failed to validate project directory") + }) + }) + + t.Run("failed to validate project name", func(t *testing.T) { + mockey.PatchConvey("mock util.ValidateProjectDir and util.ValidateProjectName", t, func() { + mockey.Mock(util.ValidateProjectDir).Return(nil).Build() + mockey.Mock(util.ValidateProjectName). + Return(errors.New("failed to validate project name")).Build() + + opts := NewOptions() + err := opts.Validate() + assert.ErrorContains(t, err, "failed to validate project name") + }) }) -} -func TestTemplatesToOptionArrayAndMap(t *testing.T) { - testTpl := scaffold.Template{ - Dir: "test", - Name: "test", - ProjectTemplate: &scaffold.ProjectTemplate{ - Description: "test", - Quickstart: "test", - StackTemplates: []*scaffold.StackTemplate{}, - }, - } - type args struct { - templates []scaffold.Template - } - tests := []struct { - name string - args args - want []string - want1 map[string]scaffold.Template - }{ - { - name: "t1", - args: args{ - templates: []scaffold.Template{ - testTpl, - }, - }, - want: []string{"test test"}, - want1: map[string]scaffold.Template{ - "test test": testTpl, - }, - }, - } - for _, tt := range tests { - mockey.PatchConvey(tt.name, t, func() { - got, got1 := templatesToOptionArrayAndMap(tt.args.templates) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("templatesToOptionArrayAndMap() got = %v, want %v", got, tt.want) - } - if !reflect.DeepEqual(got1, tt.want1) { - t.Errorf("templatesToOptionArrayAndMap() got1 = %v, want %v", got1, tt.want1) - } + t.Run("successfully validate the options", func(t *testing.T) { + mockey.PatchConvey("mock util.ValidateProjectDir and util.ValidateProjectName", t, func() { + mockey.Mock(util.ValidateProjectDir).Return(nil).Build() + mockey.Mock(util.ValidateProjectName).Return(nil).Build() + + opts := NewOptions() + err := opts.Validate() + assert.Nil(t, err) }) - } + }) } -func TestPromptValue(t *testing.T) { - valueType := "project-name" - defaultValue := "foo" - description := "project name" - flag := true - isValidFunc := func(value string) error { - flag = !flag - if !flag { - return fmt.Errorf("invalid value: %s", value) - } else { - return nil - } - } - - // mock survey.AskOne - mockey.Mock(survey.AskOne).To(func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error { - reflect.ValueOf(response).Elem().Set(reflect.ValueOf(defaultValue)) - return nil - }).Build() - - mockey.PatchConvey("prompt success", t, func() { - got, err := promptValue(valueType, description, defaultValue, nil) - assert.Nil(t, err) - assert.Equal(t, defaultValue, got) +func TestOptions_Run(t *testing.T) { + t.Run("failed to generate demo project", func(t *testing.T) { + mockey.PatchConvey("mock scaffold.GenDemoProject", t, func() { + mockey.Mock(scaffold.GenDemoProject). + Return(errors.New("failed to generate demo project")).Build() + + opts := NewOptions() + err := opts.Run() + assert.ErrorContains(t, err, "failed to generate demo project") + }) }) - mockey.PatchConvey("valid failed first and succeed next", t, func() { - got, err := promptValue(valueType, description, defaultValue, isValidFunc) - assert.Nil(t, err) - assert.Equal(t, defaultValue, got) + + t.Run("successfully initiate the demo project", func(t *testing.T) { + mockey.PatchConvey("mock scaffold.GenDemoProject", t, func() { + mockey.Mock(scaffold.GenDemoProject).Return(nil).Build() + + opts := NewOptions() + err := opts.Run() + assert.Nil(t, err) + }) }) } diff --git a/pkg/cmd/init/util/util.go b/pkg/cmd/init/util/util.go new file mode 100644 index 00000000..f74d8b1e --- /dev/null +++ b/pkg/cmd/init/util/util.go @@ -0,0 +1,67 @@ +package util + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" +) + +var ( + ErrNotEmptyDir = errors.New("not empty directory for initialization") + ErrEmptyProjectName = errors.New("the project name must not be empty") + ErrProjectNameTooLong = errors.New("the project name must be less than 100 characters") + ErrProjectNameInvalid = errors.New("the project name can only contain alphanumeric, hyphens, underscores, and periods") +) + +// Naming rules are backend-specific. However, we provide baseline sanitization for project names +// in this file. Though the backend may enforce stronger restrictions for a project name or description +// further down the line. +var ( + validProjectNameRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$") +) + +// GetDirAndName returns the rooted path and the last element of the current working directory +// for the initialized demo project. +func GetDirAndName() (dir, name string, err error) { + dir, err = os.Getwd() + if err != nil { + return "", "", fmt.Errorf("failed to get the path of the current directory: %v", err) + } + name = filepath.Base(dir) + + return dir, name, nil +} + +// ValidateProjectDir ensures the project directory for initialization is valid. +func ValidateProjectDir(dir string) error { + files, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to read the current directory: %v", err) + } + + // The demo project directory to be initialized needs to be empty initially. + if len(files) > 0 { + return ErrNotEmptyDir + } + + return nil +} + +// ValidateProjectName ensures a project name is valid. +func ValidateProjectName(name string) error { + if name == "" { + return ErrEmptyProjectName + } + + if len(name) > 100 { + return ErrProjectNameTooLong + } + + if !validProjectNameRegexp.MatchString(name) { + return ErrProjectNameInvalid + } + + return nil +} diff --git a/pkg/cmd/init/util/util_test.go b/pkg/cmd/init/util/util_test.go new file mode 100644 index 00000000..38e85a44 --- /dev/null +++ b/pkg/cmd/init/util/util_test.go @@ -0,0 +1,125 @@ +package util + +import ( + "errors" + "os" + "strings" + "testing" + + "github.com/bytedance/mockey" + "github.com/stretchr/testify/assert" +) + +func TestGetDirAndName(t *testing.T) { + t.Run("failed to get the path of the current directory", func(t *testing.T) { + mockey.PatchConvey("mock os.Getwd", t, func() { + mockey.Mock(os.Getwd).To(func() (dir string, err error) { + return "", errors.New("failed to get the path of the current directory") + }).Build() + + _, _, err := GetDirAndName() + assert.ErrorContains(t, err, "failed to get the path of the current directory") + }) + }) + + t.Run("successfully get directory and name", func(t *testing.T) { + mockey.PatchConvey("mock os.Getwd", t, func() { + mockey.Mock(os.Getwd).To(func() (dir string, err error) { + return "/dir/to/demo-project", nil + }).Build() + + dir, name, err := GetDirAndName() + assert.Nil(t, err) + assert.Equal(t, "/dir/to/demo-project", dir) + assert.Equal(t, "demo-project", name) + }) + }) +} + +func TestValidateProjectDir(t *testing.T) { + // Create temporary project directory for unit test. + tmpTestRootDir, err := os.MkdirTemp("", "kusion-test-init-util") + if err != nil { + t.Fatalf("failed to create temporary test root directory: %v", err) + } + defer os.RemoveAll(tmpTestRootDir) + + tmpProjectDir, err := os.MkdirTemp(tmpTestRootDir, "quickstart-test") + if err != nil { + t.Fatalf("failed to create temporary project directory: %v", err) + } + + testcases := []struct { + name string + dir string + expectedErr error + }{ + { + name: "directory not exists", + dir: "/dir/to/project/not/exists", + expectedErr: errors.New("failed to read the current directory"), + }, + { + name: "directory not empty", + dir: tmpTestRootDir, + expectedErr: ErrNotEmptyDir, + }, + { + name: "successfully validate the project directory", + dir: tmpProjectDir, + expectedErr: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + actualErr := ValidateProjectDir(tc.dir) + if tc.expectedErr != nil { + assert.ErrorContains(t, actualErr, tc.expectedErr.Error()) + } else { + assert.NoError(t, actualErr) + } + }) + } +} + +func TestValidateProjectName(t *testing.T) { + testcases := []struct { + name string + projectName string + expectedErr error + }{ + { + name: "empty project name", + projectName: "", + expectedErr: ErrEmptyProjectName, + }, + { + name: "more than 100 characters", + projectName: strings.Repeat("a", 101), + expectedErr: ErrProjectNameTooLong, + }, + { + name: "not match the regex", + projectName: "quickstart-^_^", + expectedErr: ErrProjectNameInvalid, + }, + { + name: "valid project name", + projectName: "quickstart", + expectedErr: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + actualErr := ValidateProjectName(tc.projectName) + + if tc.expectedErr != nil { + assert.ErrorContains(t, actualErr, tc.expectedErr.Error()) + } else { + assert.NoError(t, actualErr) + } + }) + } +} diff --git a/pkg/scaffold/demo_loader.go b/pkg/scaffold/demo_loader.go new file mode 100644 index 00000000..89c51118 --- /dev/null +++ b/pkg/scaffold/demo_loader.go @@ -0,0 +1,93 @@ +package scaffold + +import ( + "embed" + "errors" + "io/fs" + "os" + "path/filepath" + "text/template" + + v1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/workspace" +) + +const demoTmplDir = "quickstart" + +//go:embed quickstart +var demoFS embed.FS + +// GenDemoProject creates the demo project with a specified name in the specified directory. +func GenDemoProject(dir, name string) error { + // Create the default workspace for the initialized demo project if not exists. + _, err := workspace.GetWorkspaceByDefaultOperator("default") + if err != nil { + ws := &v1.Workspace{ + Name: "default", + } + + if err = workspace.CreateWorkspaceByDefaultOperator(ws); err != nil { + if !errors.Is(err, workspace.ErrWorkspaceAlreadyExist) { + return err + } + } + } + + // Define the embeded template parameter data. + data := struct { + ProjectName string + }{ + ProjectName: name, + } + + // Walk through the embeded template and creates the demo project with the specified name in the specified directory. + err = fs.WalkDir(demoFS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip the top-level root directory of the embeded template. + relPath, err := filepath.Rel(demoTmplDir, path) + if err != nil { + return err + } + if relPath == "" || relPath == "." { + return nil + } + + dstPath := filepath.Join(dir, relPath) + if d.IsDir() { + if err := os.MkdirAll(dstPath, os.ModePerm); err != nil { + return err + } + } else { + srcFile, err := demoFS.ReadFile(path) + if err != nil { + return err + } + + dstFile, err := os.Create(dstPath) + if err != nil { + return err + } + defer dstFile.Close() + + tmpl, err := template.New(filepath.Base(path)).Parse(string(srcFile)) + if err != nil { + return err + } + + if err = tmpl.Execute(dstFile, data); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} diff --git a/pkg/scaffold/demo_loader_test.go b/pkg/scaffold/demo_loader_test.go new file mode 100644 index 00000000..94ba9b76 --- /dev/null +++ b/pkg/scaffold/demo_loader_test.go @@ -0,0 +1,52 @@ +package scaffold + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/bytedance/mockey" + "github.com/stretchr/testify/assert" + v1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/workspace" +) + +func TestGenDemoProject(t *testing.T) { + // Create temporary project directory for unit test. + tmpProjectDir, err := os.MkdirTemp("", "kusion-quickstart-test") + if err != nil { + t.Fatalf("failed to create temporary project directory: %v", err) + } + defer os.RemoveAll(tmpProjectDir) + + t.Run("failed to create default workspace", func(t *testing.T) { + mockey.PatchConvey("mock workspace related functions", t, func() { + mockey.Mock(workspace.GetWorkspaceByDefaultOperator). + To(func(name string) (*v1.Workspace, error) { + return &v1.Workspace{}, workspace.ErrWorkspaceNotExist + }).Build() + mockey.Mock(workspace.CreateWorkspaceByDefaultOperator). + To(func(ws *v1.Workspace) error { + return errors.New("failed to create default workspace") + }).Build() + + err := GenDemoProject("/dir/to/quickstart", "quickstart") + assert.ErrorContains(t, err, "failed to create default workspace") + }) + }) + + t.Run("failed to create destination directory or file path", func(t *testing.T) { + mockey.PatchConvey("mock workspace related function", t, func() { + mockey.Mock(workspace.GetWorkspaceByDefaultOperator).Return(nil, nil).Build() + + err := GenDemoProject("/dir/to/kusion-project/not-exists", "not-exists") + assert.NotNil(t, err) + }) + }) + + t.Run("successfully creates the demo project", func(t *testing.T) { + err := GenDemoProject(tmpProjectDir, filepath.Base(tmpProjectDir)) + assert.Nil(t, err) + }) +} diff --git a/pkg/scaffold/internal/single-stack-sample/dev/stack.yaml b/pkg/scaffold/internal/single-stack-sample/dev/stack.yaml deleted file mode 100644 index 1be7264f..00000000 --- a/pkg/scaffold/internal/single-stack-sample/dev/stack.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# The stack basic info -name: dev diff --git a/pkg/scaffold/internal/single-stack-sample/kusion.yaml b/pkg/scaffold/internal/single-stack-sample/kusion.yaml deleted file mode 100644 index cbdf2ae6..00000000 --- a/pkg/scaffold/internal/single-stack-sample/kusion.yaml +++ /dev/null @@ -1,13 +0,0 @@ -projectName: single-stack-sample -description: A minimal kusion project of single stack -projectFields: - - name: AppName - description: The Application Name. - type: string - default: nginx - - name: Image - description: The Image Address. - type: string - default: nginx -stacks: - - name: dev diff --git a/pkg/scaffold/internal/single-stack-sample/project.yaml b/pkg/scaffold/internal/single-stack-sample/project.yaml deleted file mode 100644 index 075eb579..00000000 --- a/pkg/scaffold/internal/single-stack-sample/project.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# The project basic info -name: {{ .ProjectName }} -generator: - type: AppConfiguration diff --git a/pkg/scaffold/internal_templates.go b/pkg/scaffold/internal_templates.go deleted file mode 100644 index d92895bb..00000000 --- a/pkg/scaffold/internal_templates.go +++ /dev/null @@ -1,104 +0,0 @@ -package scaffold - -import ( - "embed" - "io/fs" - "os" - "path/filepath" - - "github.com/spf13/afero" -) - -var ( - //go:embed internal - internalTemplates embed.FS - - internalDir = "internal" -) - -// GenInternalTemplates save localTemplates(FS) to internal-templates(target directory). -func GenInternalTemplates() error { - baseTemplateDir, err := GetTemplateDir(BaseTemplateDir) - if err != nil { - return err - } - return fs.WalkDir(internalTemplates, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - destDir := filepath.Join(baseTemplateDir, path) - err := os.MkdirAll(destDir, os.ModePerm) - if err != nil { - return err - } - } else { - bytes, err := internalTemplates.ReadFile(path) - if err != nil { - return err - } - destFile := filepath.Join(baseTemplateDir, path) - err = writeAllBytes(destFile, bytes, true, os.ModePerm) - if err != nil { - return err - } - } - return nil - }) -} - -// GetInternalTemplates exports internal templates which is embed in binary. -func GetInternalTemplates() embed.FS { - return internalTemplates -} - -// Transfer embed.FS into afero.Fs. -func Transfer(srcFS embed.FS) (afero.Fs, error) { - destFS := afero.NewMemMapFs() - return destFS, fs.WalkDir(srcFS, internalDir, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - if err = destFS.MkdirAll(path, DefaultDirectoryPermission); err != nil { - return err - } - } else { - // Read source file content - content, err := fs.ReadFile(internalTemplates, path) - if err != nil { - return err - } - // Create or Update - writer, err := destFS.OpenFile(path, CreateOrUpdate, DefaultFilePermission) - if err != nil { - return err - } - defer func() { - if closeErr := writer.Close(); err == nil && closeErr != nil { - err = closeErr - } - }() - // Write into FS - if _, err := writer.Write(content); err != nil { - return err - } - } - return nil - }) -} - -// InternalTemplateNameToPath return a map of template name to path. -func InternalTemplateNameToPath() map[string]string { - schemaToPath := make(map[string]string) - dirs, err := fs.ReadDir(internalTemplates, internalDir) - if err != nil { - return schemaToPath - } - for _, dir := range dirs { - if dir.IsDir() { - schemaToPath[dir.Name()] = filepath.Join(internalDir, dir.Name()) - } - } - return schemaToPath -} diff --git a/pkg/scaffold/internal_templates_test.go b/pkg/scaffold/internal_templates_test.go deleted file mode 100644 index 82f31abb..00000000 --- a/pkg/scaffold/internal_templates_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package scaffold - -import ( - "io/fs" - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" -) - -func TestGenInternalTemplates(t *testing.T) { - targetDir, err := GetTemplateDir(InternalTemplateDir) - assert.Nil(t, err) - err = GenInternalTemplates() - assert.Nil(t, err) - srcDir := "internal" - - var checkFiles func(src, target string) error - checkFiles = func(src, target string) error { - srcFileInfos, err := os.ReadDir(src) - if err != nil { - return err - } - targetFileInfos, err := os.ReadDir(target) - if err != nil { - return err - } - for i := range srcFileInfos { - srcFileInfo := srcFileInfos[i] - if srcFileInfo.Name() == KusionYaml { - // kusion.yaml is not rendered - continue - } - targetFileInfo := targetFileInfos[i] - assert.Equal(t, srcFileInfo.IsDir(), targetFileInfo.IsDir()) - assert.Equal(t, srcFileInfo.Name(), targetFileInfo.Name()) - srcPath := filepath.Join(src, srcFileInfo.Name()) - targetPath := filepath.Join(target, targetFileInfo.Name()) - if srcFileInfo.IsDir() && targetFileInfo.IsDir() { - // recursive check - return checkFiles(srcPath, targetPath) - } else if !srcFileInfo.IsDir() && !targetFileInfo.IsDir() { - // read content - srcBytes, err := os.ReadFile(srcPath) - if err != nil { - return err - } - targetBytes, err := os.ReadFile(targetPath) - if err != nil { - return err - } - assert.Equal(t, srcBytes, targetBytes) - } - } - return nil - } - // check files tree - err = checkFiles(srcDir, targetDir) - assert.Nil(t, err) -} - -func TestInternalSchemas(t *testing.T) { - schemas := InternalTemplateNameToPath() - path, ok := schemas[templateName] - assert.True(t, ok) - assert.Equal(t, path, filepath.Join(templateDir, templateName)) -} - -func Test_readIntoFS(t *testing.T) { - local := afero.NewMemMapFs() - err := ReadTemplate(internalDir, local) - assert.Nil(t, err) - - got := []string{} - err = afero.Walk(local, filepath.Join(templateDir, templateName), func(path string, info fs.FileInfo, err error) error { - got = append(got, path) - return nil - }) - assert.Nil(t, err) - - want := []string{ - "internal/single-stack-sample", - "internal/single-stack-sample/dev", - "internal/single-stack-sample/dev/kcl.mod", - "internal/single-stack-sample/dev/main.k", - "internal/single-stack-sample/dev/stack.yaml", - "internal/single-stack-sample/kusion.yaml", - "internal/single-stack-sample/project.yaml", - } - assert.Equal(t, want, got) -} diff --git a/pkg/scaffold/loader.go b/pkg/scaffold/loader.go deleted file mode 100644 index 19dd2d21..00000000 --- a/pkg/scaffold/loader.go +++ /dev/null @@ -1,54 +0,0 @@ -package scaffold - -import ( - "errors" - "os" - "sync" - - "gopkg.in/yaml.v3" -) - -// projectTemplateSingleton is a singleton instance of projectTemplateLoader, which controls a global map of instances of ProjectTemplate -// configs (one per path). -var projectTemplateSingleton = &projectTemplateLoader{ - internal: map[string]*ProjectTemplate{}, -} - -// projectTemplateLoader is used to load a single global instance of a ProjectTemplate config. -type projectTemplateLoader struct { - sync.RWMutex - internal map[string]*ProjectTemplate -} - -// LoadProjectTemplate reads a project definition from a file. -func LoadProjectTemplate(path string) (*ProjectTemplate, error) { - if path == "" { - return nil, errors.New("path is empty") - } - - return projectTemplateSingleton.load(path) -} - -// Load a ProjectTemplate config file from the specified path. The configuration will be cached for subsequent loads. -func (singleton *projectTemplateLoader) load(path string) (*ProjectTemplate, error) { - singleton.Lock() - defer singleton.Unlock() - - if v, ok := singleton.internal[path]; ok { - return v, nil - } - - b, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var project ProjectTemplate - err = yaml.Unmarshal(b, &project) - if err != nil { - return nil, err - } - - singleton.internal[path] = &project - return &project, nil -} diff --git a/pkg/scaffold/path.go b/pkg/scaffold/path.go deleted file mode 100644 index d6d89f29..00000000 --- a/pkg/scaffold/path.go +++ /dev/null @@ -1,14 +0,0 @@ -package scaffold - -const ( - // BaseTemplateDir is the name of the directory containing templates. - BaseTemplateDir = "templates" - // InternalTemplateDir is the name of the directory containing internal templates. - InternalTemplateDir = "templates/internal" - // ExternalTemplateDir is the name of the directory containing external templates. - ExternalTemplateDir = "templates/external" - // GitDir is the name of the folder git uses to store information. - GitDir = ".git" - // KusionYaml is a config file which describe all params of a template - KusionYaml = "kusion.yaml" -) diff --git a/pkg/scaffold/project.go b/pkg/scaffold/project.go deleted file mode 100644 index 5b597de4..00000000 --- a/pkg/scaffold/project.go +++ /dev/null @@ -1,85 +0,0 @@ -package scaffold - -import ( - "fmt" - "strconv" -) - -// ProjectTemplate is a Kusion project template manifest. -type ProjectTemplate struct { - // ProjectName is a required fully qualified name. - ProjectName string `json:"projectName" yaml:"projectName"` - // Description is an optional description of the template. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - // Quickstart contains optional text to be displayed after template creation. - Quickstart string `json:"quickstart,omitempty" yaml:"quickstart,omitempty"` - // ProjectFields contains configuration in project level - ProjectFields []*FieldTemplate `json:"projectFields,omitempty" yaml:"projectFields,omitempty"` - // StackTemplates contains configuration in stack level - StackTemplates []*StackTemplate `json:"stacks,omitempty" yaml:"stacks,omitempty"` -} - -// StackTemplate contains configuration in stack level -type StackTemplate struct { - // Name is stack name - Name string `json:"name" yaml:"name"` - // Fields contains all stack fields definition - Fields []*FieldTemplate `json:"fields,omitempty" yaml:"fields,omitempty"` -} - -// FieldTemplate can describe all kinds of type, including primitive and composite. -type FieldTemplate struct { - // Name represents the field name, required - Name string `json:"name,omitempty" yaml:"name,omitempty"` - // Description represents the field description, optional - Description string `json:"description,omitempty" yaml:"description,omitempty"` - // Type can be string/int/bool/float/array/map/struct/any, required - Type FieldType `json:"type,omitempty" yaml:"type,omitempty"` - // Default represents default value for all FieldType - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` - // Elem is effective only when type is ArrayField - Elem *FieldTemplate `json:"elem,omitempty" yaml:"elem,omitempty"` - // Key is effective only when type is MapField - Key *FieldTemplate `json:"key,omitempty" yaml:"key,omitempty"` - // Value is effective only when type is MapField - Value *FieldTemplate `json:"value,omitempty" yaml:"value,omitempty"` - // Fields is effective only when type is StructField - Fields []*FieldTemplate `json:"fields,omitempty" yaml:"fields,omitempty"` -} - -// FieldType includes field type that can be unmarshalled directly -type FieldType string - -const ( - StringField FieldType = "string" - IntField FieldType = "int" - BoolField FieldType = "bool" - FloatField FieldType = "float" - ArrayField FieldType = "array" - MapField FieldType = "map" - StructField FieldType = "struct" - AnyField FieldType = "any" // AnyField equal to interface{} -) - -// IsPrimitive indicate the give field is one of StringField, IntField, FloatField, BoolField or not -func (f FieldType) IsPrimitive() bool { - return f == StringField || f == IntField || f == FloatField || f == BoolField -} - -// RestoreActualValue help to transfer input to actual value according to its type -func (f *FieldTemplate) RestoreActualValue(input string) (actual interface{}, err error) { - if f.Type == "" { - return nil, fmt.Errorf("field %s miss type definition", f.Name) - } - switch f.Type { - case IntField: - actual, err = strconv.Atoi(input) - case BoolField: - actual, err = strconv.ParseBool(input) - case FloatField: - actual, err = strconv.ParseFloat(input, 64) - case StringField: - return input, nil - } - return actual, err -} diff --git a/pkg/scaffold/project_test.go b/pkg/scaffold/project_test.go deleted file mode 100644 index c1cde8f4..00000000 --- a/pkg/scaffold/project_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package scaffold - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFieldTemplate_RestoreActualValue(t *testing.T) { - type fields struct { - Name string - Description string - Type FieldType - Default interface{} - Elem *FieldTemplate - Key *FieldTemplate - Value *FieldTemplate - Fields []*FieldTemplate - } - type args struct { - input string - } - tests := []struct { - name string - fields fields - args args - wantActual interface{} - wantErr bool - }{ - { - name: "string", - fields: fields{ - Type: StringField, - }, - args: args{ - input: "foo", - }, - wantActual: "foo", - wantErr: false, - }, - { - name: "array", - fields: fields{ - Type: ArrayField, - Elem: &FieldTemplate{ - Type: IntField, - }, - Default: []int{1, 2, 3}, - }, - wantActual: nil, - wantErr: false, - }, - { - name: "map", - fields: fields{ - Type: MapField, - Key: &FieldTemplate{ - Type: StringField, - }, - Value: &FieldTemplate{ - Type: BoolField, - }, - Default: map[string]bool{ - "foo": true, - "bar": false, - }, - }, - wantActual: nil, - wantErr: false, - }, - { - name: "struct", - fields: fields{ - Type: StructField, - Fields: []*FieldTemplate{ - { - Name: "float field", - Type: FloatField, - }, - { - Name: "array field", - Type: ArrayField, - Elem: &FieldTemplate{ - Type: IntField, - }, - }, - { - Name: "map field", - Type: MapField, - Key: &FieldTemplate{ - Type: StringField, - }, - Value: &FieldTemplate{ - Type: BoolField, - }, - }, - { - Name: "inner struct", - Type: StructField, - Fields: []*FieldTemplate{ - { - Name: "foo", - Type: StringField, - }, - }, - }, - }, - Default: map[string]interface{}{ - "string field": "foo", - "array field": []int{1, 2, 3}, - "map field": map[string]bool{ - "foo": true, - "bar": false, - }, - "inner struct": map[string]interface{}{ - "foo": "bar", - }, - }, - }, - wantActual: nil, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := &FieldTemplate{ - Name: tt.fields.Name, - Description: tt.fields.Description, - Type: tt.fields.Type, - Default: tt.fields.Default, - Elem: tt.fields.Elem, - Key: tt.fields.Key, - Value: tt.fields.Value, - Fields: tt.fields.Fields, - } - gotActual, err := f.RestoreActualValue(tt.args.input) - assert.Equalf(t, tt.wantActual, gotActual, "RestoreActualValue(%v)", tt.args.input) - assert.Equalf(t, tt.wantErr, err != nil, "RestoreActualValue(%v), err: %v", tt.args.input, err) - }) - } -} diff --git a/pkg/scaffold/internal/single-stack-sample/dev/kcl.mod b/pkg/scaffold/quickstart/dev/kcl.mod similarity index 84% rename from pkg/scaffold/internal/single-stack-sample/dev/kcl.mod rename to pkg/scaffold/quickstart/dev/kcl.mod index a9e9b74d..18272603 100644 --- a/pkg/scaffold/internal/single-stack-sample/dev/kcl.mod +++ b/pkg/scaffold/quickstart/dev/kcl.mod @@ -1,9 +1,8 @@ [package] -name = "{{ .ProjectName }}" +name = "kusion-quickstart" version = "0.1.0" [dependencies] catalog = { git = "https://github.com/KusionStack/catalog.git", tag = "0.1.2" } - [profile] entries = ["main.k"] diff --git a/pkg/scaffold/internal/single-stack-sample/dev/main.k b/pkg/scaffold/quickstart/dev/main.k similarity index 54% rename from pkg/scaffold/internal/single-stack-sample/dev/main.k rename to pkg/scaffold/quickstart/dev/main.k index 2f170445..499a1d9f 100644 --- a/pkg/scaffold/internal/single-stack-sample/dev/main.k +++ b/pkg/scaffold/quickstart/dev/main.k @@ -1,23 +1,19 @@ import catalog.models.schema.v1 as ac import catalog.models.schema.v1.workload as wl -import catalog.models.schema.v1.workload.network as n import catalog.models.schema.v1.workload.container as c +import catalog.models.schema.v1.workload.network as n -{{ .AppName }}: ac.AppConfiguration { +# main.k declares the customized configuration codes for dev stack. +quickstart: ac.AppConfiguration { workload: wl.Service { containers: { - "{{ .AppName }}": c.Container { - image = "{{ .Image }}" - resources: { - "cpu": "500m" - "memory": "512Mi" - } + quickstart: c.Container { + image: "kusionstack/kusion-quickstart:latest" } } - replicas: 2 ports: [ n.Port { - port: 80 + port: 8080 } ] } diff --git a/pkg/scaffold/quickstart/dev/stack.yaml b/pkg/scaffold/quickstart/dev/stack.yaml new file mode 100644 index 00000000..073ea986 --- /dev/null +++ b/pkg/scaffold/quickstart/dev/stack.yaml @@ -0,0 +1,2 @@ +# The stack basic info. +name: dev diff --git a/pkg/scaffold/quickstart/project.yaml b/pkg/scaffold/quickstart/project.yaml new file mode 100644 index 00000000..7b674465 --- /dev/null +++ b/pkg/scaffold/quickstart/project.yaml @@ -0,0 +1,2 @@ +# The project basic info. +name: {{ .ProjectName }} diff --git a/pkg/scaffold/templates.go b/pkg/scaffold/templates.go deleted file mode 100644 index f36cc731..00000000 --- a/pkg/scaffold/templates.go +++ /dev/null @@ -1,599 +0,0 @@ -package scaffold - -import ( - "bytes" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "regexp" - "strings" - "text/template" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/gitutil" - "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" - "github.com/spf13/afero" - "github.com/texttheater/golang-levenshtein/levenshtein" - - "kusionstack.io/kusion/pkg/log" - "kusionstack.io/kusion/pkg/util/kfile" -) - -// These are variables instead of constants in order that they can be set using the `-X` -// `ldflag` at build time, if necessary. -var ( - // KusionTemplateGitRepository is the Git URL for Kusion program templates - KusionTemplateGitRepository = "https://github.com/KusionStack/kusion-templates" - // The branch name for the template repository - kusionTemplateBranch = "main" -) - -// TemplateRepository represents a repository of templates. -type TemplateRepository struct { - Root string // The full path to the root directory of the repository. - SubDirectory string // The full path to the subdirectory within the repository. - ShouldDelete bool // Whether the root directory should be deleted. -} - -// Delete deletes the template repository. -func (repo TemplateRepository) Delete() error { - if repo.ShouldDelete { - return os.RemoveAll(repo.Root) - } - return nil -} - -// Templates lists the templates in the repository. -func (repo TemplateRepository) Templates() ([]Template, error) { - path := repo.SubDirectory - - info, err := os.Stat(path) - if err != nil { - return nil, err - } - - // If it's a file, look in its directory. - if !info.IsDir() { - path = filepath.Dir(path) - } - - // See if there's a kusion.yaml in the directory. - t, err := LoadTemplate(path) - if err != nil && !os.IsNotExist(err) { - return nil, err - } else if err == nil { - return []Template{t}, nil - } - - // Otherwise, read all subdirectories to find the ones - // that contain a kusion.yaml. - infos, err := os.ReadDir(path) - if err != nil { - return nil, err - } - - var result []Template - for _, info := range infos { - if info.IsDir() { - name := info.Name() - - // Ignore the .git directory. - if name == GitDir { - continue - } - - loadTemplate, err := LoadTemplate(filepath.Join(path, name)) - if err != nil && !os.IsNotExist(err) { - return nil, err - } else if err == nil { - result = append(result, loadTemplate) - } - } - } - return result, nil -} - -// LoadTemplate returns a template from a path. -func LoadTemplate(path string) (Template, error) { - info, err := os.Stat(path) - if err != nil { - return Template{}, err - } - if !info.IsDir() { - return Template{}, fmt.Errorf("%s is not a directory", path) - } - - proj, err := LoadProjectTemplate(filepath.Join(path, KusionYaml)) - if err != nil { - return Template{}, err - } - - t := Template{ - Dir: path, - Name: filepath.Base(path), - } - if proj != nil { - t.ProjectTemplate = proj - } - - return t, nil -} - -// Template represents a project template. -type Template struct { - // The directory containing kusion.yaml. - Dir string `json:"dir,omitempty" yaml:"dir,omitempty"` - // The name of the template. - Name string `json:"name,omitempty" yaml:"name,omitempty"` - - *ProjectTemplate -} - -// RetrieveTemplates retrieves a "template repository" based on the specified name, path, or URL. -func RetrieveTemplates(templateNamePathOrURL string, online bool) (TemplateRepository, error) { - if IsTemplateURL(templateNamePathOrURL) { - return retrieveURLTemplates(templateNamePathOrURL, online) - } - if isTemplateFileOrDirectory(templateNamePathOrURL) { - return retrieveFileTemplates(templateNamePathOrURL) - } - return retrieveKusionTemplates(templateNamePathOrURL, online) -} - -// IsTemplateURL returns true if templateNamePathOrURL starts with "https://". -func IsTemplateURL(templateNamePathOrURL string) bool { - return strings.HasPrefix(templateNamePathOrURL, "https://") -} - -// retrieveURLTemplates retrieves the "template repository" at the specified URL. -func retrieveURLTemplates(rawURL string, online bool) (TemplateRepository, error) { - if !online { - return TemplateRepository{}, fmt.Errorf("cannot use %s offline", rawURL) - } - - var err error - - // Create a temp dir. - var temp string - if temp, err = os.MkdirTemp("", "kusion-template-"); err != nil { - return TemplateRepository{}, err - } - - var fullPath string - if fullPath, err = workspace.RetrieveGitFolder(rawURL, temp); err != nil { - return TemplateRepository{}, fmt.Errorf("failed to retrieve git folder: %w", err) - } - - return TemplateRepository{ - Root: temp, - SubDirectory: fullPath, - ShouldDelete: true, - }, nil -} - -// isTemplateFileOrDirectory returns true if templateNamePathOrURL is the name of a valid file or directory. -func isTemplateFileOrDirectory(templateNamePathOrURL string) bool { - _, err := os.Stat(templateNamePathOrURL) - return err == nil -} - -// retrieveFileTemplates points to the "template repository" at the specified location in the file system. -func retrieveFileTemplates(path string) (TemplateRepository, error) { - return TemplateRepository{ - Root: path, - SubDirectory: path, - ShouldDelete: false, - }, nil -} - -// retrieveKusionTemplates retrieves the "template repository" for Kusion templates. -// Instead of retrieving to a temporary directory, the Kusion templates are managed from -// ~/.kusion/templates. -func retrieveKusionTemplates(templateName string, online bool) (TemplateRepository, error) { - templateName = strings.ToLower(templateName) - - // Get the template directory. - templateDir, err := GetTemplateDir(ExternalTemplateDir) - if err != nil { - return TemplateRepository{}, err - } - - // Ensure the template directory exists. - if err = os.RemoveAll(templateDir); err != nil { - return TemplateRepository{}, err - } - if err = os.MkdirAll(templateDir, DefaultDirectoryPermission); err != nil { - return TemplateRepository{}, err - } - - if online { - // Clone or update the kusion/templates repo. - repo := KusionTemplateGitRepository - branch := plumbing.NewBranchReferenceName(kusionTemplateBranch) - err = gitutil.GitCloneOrPull(repo, branch, templateDir, false /*shallow*/) - if err != nil { - return TemplateRepository{}, fmt.Errorf("cloning templates failed. Please try again: %w", err) - } - } - - subDir := templateDir - if templateName != "" { - subDir = filepath.Join(subDir, templateName) - - // Provide a nicer error message when the template can't be found (dir doesn't exist). - _, err := os.Stat(subDir) - if err != nil { - if os.IsNotExist(err) { - return TemplateRepository{}, newTemplateNotFoundError(templateDir, templateName) - } - log.Warnf("Explicitly ignoring and discarding error: %v", err) - } - } - - return TemplateRepository{ - Root: templateDir, - SubDirectory: subDir, - ShouldDelete: false, - }, nil -} - -// GetTemplateDir returns the directory in which templates on the current machine are stored. -func GetTemplateDir(subDir string) (string, error) { - kusionDir, err := kfile.KusionDataFolder() - if err != nil { - return "", err - } - return filepath.Join(kusionDir, subDir), nil -} - -// newExistingFilesError returns a new error from a list of existing file names -// that would be overwritten. -func newExistingFilesError(existing []string) error { - if len(existing) == 0 { - return errors.New("no existing files") - } - - message := "creating this template will make changes to existing files:\n" - - for _, file := range existing { - message += fmt.Sprintf(" overwrite %s\n", file) - } - - message += "\nrerun the command and pass --force to accept and create" - - return errors.New(message) -} - -// newTemplateNotFoundError returns an error for when the template doesn't exist, -// offering distance-based suggestions in the error message. -func newTemplateNotFoundError(templateDir string, templateName string) error { - message := fmt.Sprintf("template '%s' not found", templateName) - - // Attempt to read the directory to offer suggestions. - infos, err := os.ReadDir(templateDir) - if err != nil { - log.Errorf("os.ReadDir(%s) error: %v", templateDir, err) - return errors.New(message) - } - - // Get suggestions based on levenshtein distance. - suggestions := []string{} - const minDistance = 2 - op := levenshtein.DefaultOptions - for _, info := range infos { - distance := levenshtein.DistanceForStrings([]rune(templateName), []rune(info.Name()), op) - if distance <= minDistance { - suggestions = append(suggestions, info.Name()) - } - } - - // Build-up error message with suggestions. - if len(suggestions) > 0 { - message += "\n\nDid you mean this?\n" - for _, suggestion := range suggestions { - message += fmt.Sprintf("\t%s\n", suggestion) - } - } - - return errors.New(message) -} - -// Naming rules are backend-specific. However, we provide baseline sanitization for project names -// in this file. Though the backend may enforce stronger restrictions for a project name or description -// further down the line. -var ( - validProjectNameRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$") -) - -// ValidateProjectName ensures a project name is valid, if it is not it returns an error with a message suitable -// for display to an end user. -func ValidateProjectName(s string) error { - if s == "" { - return errors.New("the project name must not be empty") - } - - if len(s) > 100 { - return errors.New("the project name must be less than 100 characters") - } - - if !validProjectNameRegexp.MatchString(s) { - return errors.New("the project name can only contain alphanumeric, hyphens, underscores, and periods") - } - - return nil -} - -// TemplateConfig contains all config items to render the chosen project -type TemplateConfig struct { - // ProjectName is project name, as well as root dir name - ProjectName string `json:"projectName"` - // ProjectConfig contains configuration in project level - ProjectConfig map[string]interface{} `json:"projectConfig,omitempty"` - // StacksConfig contains configuration in stack level, can be multi-stack or single-stack - StacksConfig map[string]map[string]interface{} `json:"stacksConfig,omitempty"` -} - -var ( - // CreateOrUpdate is file flag, create or upate - CreateOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC - // DefaultDirectoryPermission is default directory permission, 700 - DefaultDirectoryPermission os.FileMode = 0o700 - // DefaultFilePermission is default file permission, 600 - DefaultFilePermission os.FileMode = 0o600 -) - -// RenderLocalTemplate does the actual copy operation from source directory to a destination directory. -func RenderLocalTemplate(sourceDir, destDir string, force bool, tc *TemplateConfig) error { - // source FS - srcFS := afero.NewMemMapFs() - if err := ReadTemplate(sourceDir, srcFS); err != nil { - return err - } - // destination FS - destFS := afero.NewMemMapFs() - if err := RenderFSTemplate(srcFS, sourceDir, destFS, destDir, tc); err != nil { - return err - } - // write into disk - return WriteToDisk(destFS, destDir, force) -} - -// ReadTemplate read file content from local dir into file system. -func ReadTemplate(dir string, fs afero.Fs) error { - fileInfos, err := os.ReadDir(dir) - if err != nil { - return err - } - for _, fileInfo := range fileInfos { - path := filepath.Join(dir, fileInfo.Name()) - if fileInfo.IsDir() { - if err = fs.MkdirAll(path, DefaultDirectoryPermission); err != nil { - return err - } - if err = ReadTemplate(path, fs); err != nil { - return err - } - } else { - // Read source file content - content, err := os.ReadFile(path) - if err != nil { - return err - } - // Create or Update - writer, err := fs.OpenFile(path, CreateOrUpdate, DefaultFilePermission) - if err != nil { - return err - } - defer func() { - if closeErr := writer.Close(); err == nil && closeErr != nil { - err = closeErr - } - }() - // Write into FS - if _, err := writer.Write(content); err != nil { - return err - } - } - } - - return nil -} - -// RenderFSTemplate does the actual copy operation from source FS to destination FS. -func RenderFSTemplate(srcFS afero.Fs, srcDir string, destFS afero.Fs, destDir string, tc *TemplateConfig) error { - // Read all sub dirs and files under srcDir - fileInfos, err := afero.ReadDir(srcFS, srcDir) - if err != nil { - return err - } - for _, d := range fileInfos { - src := filepath.Join(srcDir, d.Name()) - dest := filepath.Join(destDir, d.Name()) - if d.IsDir() { - // Base dir or stack dir - fileInfo, err := srcFS.Stat(filepath.Join(src, "stack.yaml")) - if err == nil && fileInfo.Mode().IsRegular() { - // Project config can be overridden - configs := make(map[string]interface{}, len(tc.ProjectConfig)) - for k, v := range tc.ProjectConfig { - configs[k] = v - } - // Skip if stackConfigs are not provided - stackConfigs, exits := tc.StacksConfig[d.Name()] - if !exits { - continue - } - // Merge and override project config - for k, v := range stackConfigs { - configs[k] = v - } - // Walk stack dir with merged configs - err = walkFiles(srcFS, src, destFS, dest, configs) - if err != nil { - return err - } - } else { - // Stack dir nested in 3rd level or even deeper, eg: meta_app/deployed_unit/stack_dir - err = RenderFSTemplate(srcFS, src, destFS, dest, tc) - if err != nil { - return err - } - } - } else { - // project files. eg: project.yaml - err = doFile(srcFS, src, destFS, dest, d.Name(), tc.ProjectConfig) - if err != nil { - return err - } - } - } - return nil -} - -// walkFiles is a helper that walks the directories/files in a source directory -// and performs render for each item. -func walkFiles(srcFS afero.Fs, srcDir string, destFS afero.Fs, destDir string, config map[string]interface{}) error { - // Create dest directory first - err := destFS.MkdirAll(destDir, DefaultDirectoryPermission) - if err != nil { - return err - } - - // Read files and dirs under srcDir - dirs, err := afero.ReadDir(srcFS, srcDir) - if err != nil { - return err - } - for _, d := range dirs { - src := filepath.Join(srcDir, d.Name()) - dest := filepath.Join(destDir, d.Name()) - if d.IsDir() { - // Ignore the .git directory - if d.Name() == GitDir { - continue - } - // Walk sub dir, eg: template/prod/ci - err = walkFiles(srcFS, src, destFS, dest, config) - if err != nil { - return err - } - } else { - // render files, eg: base.k - err = doFile(srcFS, src, destFS, dest, d.Name(), config) - if err != nil { - return err - } - } - } - return nil -} - -// doFile render template file content and save it in destination file system -func doFile(srcFS afero.Fs, srcPath string, destFS afero.Fs, destPath, fileName string, config map[string]interface{}) error { - // Read template - srcContent, err := afero.ReadFile(srcFS, srcPath) - if err != nil { - return err - } - - // Skip kusion.yaml - if fileName == KusionYaml { - return nil - } - - // Render template file - destContent, err := render(fileName, string(srcContent), config) - if err != nil { - return err - } - - // Create or truncate the file - writer, err := destFS.OpenFile(destPath, CreateOrUpdate, DefaultFilePermission) - if err != nil { - return err - } - defer func() { - if closeErr := writer.Close(); err == nil && closeErr != nil { - err = closeErr - } - }() - - // Write into destFS - if _, err := writer.Write(destContent); err != nil { - return err - } - return nil -} - -// render parse content(string) with configMap(map[string]string) with go tmpl -func render(name string, content string, configMap map[string]interface{}) ([]byte, error) { - temp := template.New(name) - - if _, err := temp.Parse(content); err != nil { - return nil, err - } - - out := &bytes.Buffer{} - if err := temp.Execute(out, configMap); err != nil { - return nil, err - } - return out.Bytes(), nil -} - -// Walk destination file system and persist each file to local disk -func WriteToDisk(destFS afero.Fs, root string, force bool) error { - return afero.Walk(destFS, root, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return mkdirWithForce(path, force) - } else { - content, err := afero.ReadFile(destFS, path) - if err != nil { - return err - } - err = writeAllBytes(path, content, force, DefaultFilePermission) - if err != nil { - // An existing file has shown up in between the dry run and the actual copy operation. - if os.IsExist(err) { - return newExistingFilesError([]string{filepath.Base(path)}) - } - return err - } - return nil - } - }) -} - -// mkdirWithForce will ignore dir exists error when force is true -func mkdirWithForce(path string, force bool) error { - if force { - return os.MkdirAll(path, 0o700) - } - return os.Mkdir(path, 0o700) -} - -// writeAllBytes writes the bytes to the specified file, with an option to overwrite. -func writeAllBytes(filename string, bytes []byte, overwrite bool, mode os.FileMode) error { - flag := os.O_WRONLY | os.O_CREATE - if overwrite { - flag |= os.O_TRUNC - } else { - flag |= os.O_EXCL - } - - f, err := os.OpenFile(filename, flag, mode) - if err != nil { - return err - } - defer func() { - if err := f.Close(); err != nil { - log.Warnf("Explicitly ignoring and discarding error: %v", err) - } - }() - _, err = f.Write(bytes) - return err -} diff --git a/pkg/scaffold/templates_test.go b/pkg/scaffold/templates_test.go deleted file mode 100644 index 4daee8d7..00000000 --- a/pkg/scaffold/templates_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package scaffold - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/bytedance/mockey" - "github.com/go-git/go-git/v5/plumbing" - "github.com/jinzhu/copier" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/gitutil" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" -) - -const ( - templateDir = "internal" - templateName = "single-stack-sample" -) - -var ( - localRoot, err = filepath.Abs(templateDir) - localTemplateRepo = TemplateRepository{ - Root: localRoot, - SubDirectory: filepath.Join(localRoot, templateName), - } - localTemplate = Template{ - Dir: filepath.Join(localRoot, templateName), - Name: "single-stack-sample", - ProjectTemplate: &ProjectTemplate{ - ProjectName: "single-stack-sample", - Description: "A minimal kusion project of single stack", - ProjectFields: []*FieldTemplate{ - { - Name: "AppName", - Description: "The Application Name.", - Type: StringField, - Default: "nginx", - }, - { - Name: "Image", - Description: "The Image Address.", - Type: StringField, - Default: "nginx", - }, - }, - StackTemplates: []*StackTemplate{ - { - Name: "dev", - }, - }, - }, - } -) - -func TestTemplateRepository_Delete(t *testing.T) { - t.Run("should delete", func(t *testing.T) { - tmp, err := os.MkdirTemp("", "tmp-dir-for-test") - assert.Nil(t, err) - repo := TemplateRepository{ - Root: tmp, - ShouldDelete: true, - } - err = repo.Delete() - assert.Nil(t, err) - }) - - t.Run("", func(t *testing.T) { - err = localTemplateRepo.Delete() - assert.Nil(t, err) - }) -} - -func TestTemplateRepository_Templates(t *testing.T) { - t.Run("read from dir", func(t *testing.T) { - templates, err := localTemplateRepo.Templates() - assert.Nil(t, err) - assert.Equal(t, []Template{localTemplate}, templates) - }) - - t.Run("read from subdir", func(t *testing.T) { - subRepo := TemplateRepository{} - copier.Copy(&subRepo, &localTemplateRepo) - subRepo.SubDirectory = localTemplateRepo.Root - templates, err := subRepo.Templates() - assert.Nil(t, err) - assert.Contains(t, templates, localTemplate) - }) -} - -func TestLoadTemplate(t *testing.T) { - type args struct { - path string - } - tests := []struct { - name string - args args - want Template - wantErr assert.ErrorAssertionFunc - }{ - { - name: "deployment", - args: args{ - path: "internal/single-stack-sample", - }, - want: localTemplate, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return false - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := LoadTemplate(tt.args.path) - if !tt.wantErr(t, err, fmt.Sprintf("LoadTemplate(%v)", tt.args.path)) { - return - } - assert.Equalf(t, tt.want, got, "LoadTemplate(%v)", tt.args.path) - }) - } -} - -func Test_retrieveKusionTemplates(t *testing.T) { - t.Run("retrieve not exists", func(t *testing.T) { - got, err := retrieveKusionTemplates("mockTemplateName", false) - assert.NotNil(t, err) - assert.Empty(t, got) - }) -} - -func TestRetrieveTemplates(t *testing.T) { - t.Run("url templates", func(t *testing.T) { - _, err := RetrieveTemplates(KusionTemplateGitRepository, true) - assert.Nil(t, err) - }) - - t.Run("file templates", func(t *testing.T) { - _, err := RetrieveTemplates(localRoot, false) - assert.Nil(t, err) - }) - - mockey.PatchConvey("kusion templates", t, func() { - // gitutil.GitCloneOrPull has internet issue occasionally - // mock as always succeed - mockey.Mock(gitutil.GitCloneOrPull).To(func(url string, referenceName plumbing.ReferenceName, path string, shallow bool) error { - return nil - }).Build() - - _, err := RetrieveTemplates("", true) - assert.Nil(t, err) - }) -} - -func TestValidateProjectName(t *testing.T) { - type args struct { - s string - } - tests := []struct { - name string - args args - wantErr assert.ErrorAssertionFunc - }{ - { - name: "project name is empty", - args: args{ - s: "", - }, - wantErr: func(t assert.TestingT, err2 error, i ...interface{}) bool { - return true - }, - }, - { - name: "project name is not match regexp", - args: args{ - s: "!@#$%^&*()", - }, - wantErr: func(t assert.TestingT, err2 error, i ...interface{}) bool { - return true - }, - }, - { - name: "project name is valid", - args: args{ - s: "abc", - }, - wantErr: func(t assert.TestingT, err2 error, i ...interface{}) bool { - return true - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.wantErr(t, ValidateProjectName(tt.args.s), fmt.Sprintf("ValidateProjectName(%v)", tt.args.s)) - }) - } -} - -func TestRenderTemplateFiles(t *testing.T) { - // dest dir - tmp, err := os.MkdirTemp("", "tmp-dir-for-test") - assert.Nil(t, err) - defer func() { - err = os.RemoveAll(tmp) - assert.Nil(t, err) - }() - // projectConfigs - projectConfigs := make(map[string]interface{}) - for _, f := range localTemplate.ProjectFields { - projectConfigs[f.Name] = f.Default - } - // stack2Configs - stack2Configs := make(map[string]map[string]interface{}) - for _, stack := range localTemplate.StackTemplates { - configs := make(map[string]interface{}) - for _, f := range stack.Fields { - configs[f.Name] = f.Default - } - stack2Configs[stack.Name] = configs - } - err = RenderLocalTemplate(localTemplate.Dir, tmp, true, &TemplateConfig{ - ProjectName: localTemplate.ProjectName, - ProjectConfig: projectConfigs, - StacksConfig: stack2Configs, - }) - assert.Nil(t, err) -} - -func Test_RenderMemTemplateFiles(t *testing.T) { - memMapFs := afero.NewMemMapFs() - prj := "test-proj" - srcFS, _ := Transfer(GetInternalTemplates()) - err := RenderFSTemplate( - srcFS, "internal/single-stack-sample", - memMapFs, prj, - &TemplateConfig{ - ProjectName: prj, - ProjectConfig: map[string]interface{}{ - "ServiceName": "frontend-svc", - "NodePort": 30000, - "ProjectName": prj, - }, - StacksConfig: map[string]map[string]interface{}{ - "dev": { - "Stack": "dev", - "Image": "foo/bar:v1", - "ClusterName": "minikube", - }, - }, - }) - assert.Nil(t, err) - err = WriteToDisk(memMapFs, prj, true) - defer os.RemoveAll(prj) - assert.Nil(t, err) -}