Compare commits

..

61 Commits

Author SHA1 Message Date
dceda936b7 env 2024-10-07 18:55:42 +00:00
bca332b598 run dockerfile if exists 2024-10-07 18:27:10 +00:00
f1fdc48aaa Work around this bug: https://github.com/python/cpython/pull/14064 (#941)
Otherwise we sometimes see errors like:

```
cerc-webapp-deployer:   File "/root/.shiv/laconic-so_0f937aa98c2748ef9af8585d6f441dbc01546ace0d6660cbb159d1e5040aeddf/site-packages/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py", line 671, in command
cerc-webapp-deployer:     shutil.rmtree(tempdir)
cerc-webapp-deployer:   File "/usr/lib/python3.10/shutil.py", line 725, in rmtree
cerc-webapp-deployer:     _rmtree_safe_fd(fd, path, onerror)
cerc-webapp-deployer:   File "/usr/lib/python3.10/shutil.py", line 681, in _rmtree_safe_fd
cerc-webapp-deployer:     onerror(os.unlink, fullname, sys.exc_info())
cerc-webapp-deployer:   File "/usr/lib/python3.10/shutil.py", line 679, in _rmtree_safe_fd
cerc-webapp-deployer:     os.unlink(entry.name, dir_fd=topfd)
cerc-webapp-deployer: FileNotFoundError: [Errno 2] No such file or directory: 'S.gpg-agent.extra'
```

Reviewed-on: cerc-io/stack-orchestrator#941
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-28 23:17:13 +00:00
a54072de6c Add --config-ref flag. (#939)
Add a flag to re-use config.

Reviewed-on: cerc-io/stack-orchestrator#939
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-28 17:32:52 +00:00
fa21ff2627 Support uploaded config, add 'publish-webapp-deployer' and 'request-webapp-deployment' commands (#938)
This adds two new commands: `publish-webapp-deployer` and `request-webapp-deployment`.

`publish-webapp-deployer` creates a `WebappDeployer` record, which provides information to requestors like the API URL, minimum required payment, payment address, and public key to use for encrypting config.

```
$ laconic-so publish-deployer-to-registry \
  --laconic-config ~/.laconic/laconic.yml \
  --api-url https://webapp-deployer-api.dev.vaasl.io \
  --public-key-file webapp-deployer-api.dev.vaasl.io.pgp.pub  \
  --lrn lrn://laconic/deployers/webapp-deployer-api.dev.vaasl.io  \
  --min-required-payment 100000
```

`request-webapp-deployment` simplifies publishing a `WebappDeploymentRequest` and can also handle automatic payment, and encryption and upload of configuration.

```
$ laconic-so request-webapp-deployment \
  --laconic-config ~/.laconic/laconic.yml \
  --deployer lrn://laconic/deployers/webapp-deployer-api.dev.vaasl.io \
  --app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
  --env-file ~/yaml/hello.env \
  --make-payment auto
```

Related changes are included for the deploy/undeploy commands for decrypting and using config, using the payment address from the WebappDeployer record, etc.

Reviewed-on: cerc-io/stack-orchestrator#938
2024-08-27 19:55:06 +00:00
33d395e213 Add package registry stack instructions (#937)
- The instructions to `Deploy Gitea Package Registry` from build-support [readme](https://git.vdb.to/deep-stack/stack-orchestrator/src/branch/pm-update-registry-steps/stack_orchestrator/data/stacks/build-support#2-deploy-gitea-package-registry) don't seem to be in a working state
- Updated `package-registry` stack instructions to use deployment pattern

Reviewed-on: cerc-io/stack-orchestrator#937
Reviewed-by: ashwin <ashwin@noreply.git.vdb.to>
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2024-08-23 09:42:44 +00:00
75ff60752a Require payment for app deployment requests. (#928)
Adds three new options for deployment/undeployment:

```
    "--min-required-payment",
    help="Requests must have a minimum payment to be processed",

    "--payment-address",
    help="The address to which payments should be made.  Default is the current laconic account.",

    "--all-requests",
    help="Handle requests addressed to anyone (by default only requests to my payment address are examined).",
```

In this mode, requests should be designated for a particular address with the attribute `to` and include a `payment` attribute which is the tx hash for the payment.

The deployer will confirm the payment (to the right account, right amount, not used before, etc.) and then proceed with the deployment or undeployment.

Reviewed-on: cerc-io/stack-orchestrator#928
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-21 14:39:20 +00:00
44b9709717 Use Laconic version of ping-pub (#930)
Reviewed-on: cerc-io/stack-orchestrator#930
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-20 17:44:00 +00:00
e56da7dcc1 Add support for k8s pod to node affinity and taint toleration (#917)
Reviewed-on: cerc-io/stack-orchestrator#917
Reviewed-by: Thomas E Lackey <telackey@noreply.git.vdb.to>
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-15 20:32:58 +00:00
60d34217f8 More logging for webapp deployment (#923)
```
cerc-webapp-deployer: ############ DEPLOY #############
cerc-webapp-deployer: 2024-08-15 02:13:08.321991 -  - 0:00:00.000031 (step): Discovering deployment requests...
cerc-webapp-deployer: laconic -c /etc/config/laconic.yml registry record list --all --type ApplicationDeploymentRequest
cerc-webapp-deployer: 2024-08-15 02:13:08.815428 -  - 0:00:00.493420 (step): Loading known requests from /srv/deployments/autodeploy.state...
cerc-webapp-deployer: 2024-08-15 02:13:08.815626 -  - 0:00:00.000158 (step): BEGIN: Examining request bafyreigiltcdscwt7rqldnilo4ohrhgoulrlfceixde5ycewsym64sefgi
cerc-webapp-deployer: 2024-08-15 02:13:08.815645 -  - 0:00:00.000008 (step): Skipping request bafyreigiltcdscwt7rqldnilo4ohrhgoulrlfceixde5ycewsym64sefgi, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815653 -  - 0:00:00.000005 (step): DONE Examining request bafyreigiltcdscwt7rqldnilo4ohrhgoulrlfceixde5ycewsym64sefgi with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815664 -  - 0:00:00.000005 (step): BEGIN: Examining request bafyreicoxippgdwab6cz72py4rgv63rvvbsea73y62hashlhqpcsxyfkue
cerc-webapp-deployer: 2024-08-15 02:13:08.815674 -  - 0:00:00.000006 (step): Skipping request bafyreicoxippgdwab6cz72py4rgv63rvvbsea73y62hashlhqpcsxyfkue, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815684 -  - 0:00:00.000004 (step): DONE Examining request bafyreicoxippgdwab6cz72py4rgv63rvvbsea73y62hashlhqpcsxyfkue with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815692 -  - 0:00:00.000005 (step): BEGIN: Examining request bafyreih3gt44pvahnbg7ag26mlk3iie4s5m5znhygajja5dcovheti72ne
cerc-webapp-deployer: 2024-08-15 02:13:08.815705 -  - 0:00:00.000007 (step): Skipping request bafyreih3gt44pvahnbg7ag26mlk3iie4s5m5znhygajja5dcovheti72ne, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815714 -  - 0:00:00.000005 (step): DONE Examining request bafyreih3gt44pvahnbg7ag26mlk3iie4s5m5znhygajja5dcovheti72ne with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815724 -  - 0:00:00.000004 (step): BEGIN: Examining request bafyreigjnbio47rug6x5tufzc6cwfcqpl3ck3xldzotrlz5bt663dh2pua
cerc-webapp-deployer: 2024-08-15 02:13:08.815733 -  - 0:00:00.000005 (step): Skipping request bafyreigjnbio47rug6x5tufzc6cwfcqpl3ck3xldzotrlz5bt663dh2pua, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815743 -  - 0:00:00.000005 (step): DONE Examining request bafyreigjnbio47rug6x5tufzc6cwfcqpl3ck3xldzotrlz5bt663dh2pua with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815751 -  - 0:00:00.000004 (step): BEGIN: Examining request bafyreihsfno4s6lkxcp5a7g7pjj7kklrp3xaqo57mr2pz76nk3h4jukayy
cerc-webapp-deployer: 2024-08-15 02:13:08.815761 -  - 0:00:00.000006 (step): Skipping request bafyreihsfno4s6lkxcp5a7g7pjj7kklrp3xaqo57mr2pz76nk3h4jukayy, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815770 -  - 0:00:00.000005 (step): DONE Examining request bafyreihsfno4s6lkxcp5a7g7pjj7kklrp3xaqo57mr2pz76nk3h4jukayy with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815779 -  - 0:00:00.000005 (step): BEGIN: Examining request bafyreicyfyj4ncmtuy5pain2rvc67v645cg2bbsiakizvhdiwvkx7asvdy
cerc-webapp-deployer: 2024-08-15 02:13:08.815791 -  - 0:00:00.000007 (step): Skipping request bafyreicyfyj4ncmtuy5pain2rvc67v645cg2bbsiakizvhdiwvkx7asvdy, we've already seen it.
cerc-webapp-deployer: 2024-08-15 02:13:08.815800 -  - 0:00:00.000004 (step): DONE Examining request bafyreicyfyj4ncmtuy5pain2rvc67v645cg2bbsiakizvhdiwvkx7asvdy with result SKIP.
cerc-webapp-deployer: 2024-08-15 02:13:08.815808 -  - 0:00:00.000004 (step): Discovering existing app deployments...
cerc-webapp-deployer: laconic -c /etc/config/laconic.yml registry record list --all --type ApplicationDeploymentRecord
cerc-webapp-deployer: 2024-08-15 02:13:09.330655 -  - 0:00:00.514858 (step): Discovering deployment removal and cancellation requests...
cerc-webapp-deployer: laconic -c /etc/config/laconic.yml registry record list --all --type ApplicationDeploymentRemovalRequest
cerc-webapp-deployer: 2024-08-15 02:13:09.825145 -  - 0:00:00.494460 (step): Found 0 unsatisfied request(s) to process.
cerc-webapp-deployer: ############ DEPLOY SUCCESS #############
```

Reviewed-on: cerc-io/stack-orchestrator#923
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-15 02:57:47 +00:00
952389abb0 Add option to recreate deployments rather than update them. (#920)
cherry-pick from cerc-io/stack-orchestrator#912

Reviewed-on: cerc-io/stack-orchestrator#920
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
2024-08-14 20:14:40 +00:00
5c275aa622 Defensively handle errors examining app requests. (#922)
Related to cerc-io/webapp-deployment-status-api#10

There are two issues in that.  One is that the output probably changed recently, whether in the client or server, where no matching record is found by ID (Note this is specific to `laconic record get --id <v>` and does not seem to apply to the similar command to retrieve a record by name, `laconic name resolve <n>`).

Rather than returning `[]` it is now returning `[ null ]`.  This cause us to think there *was* an application record found, and we attempt to treat the `null` entry like an Application object.  That's fixed by filtering out null responses, which is a good precaution for the deployer, though I think it makes sense to ask whether this new behavior by the client/server is correct.  Seems suspicious.

The other issue is that all the defensive checks we had in place to deal with broken/bad AppDeploymentRequests were around the _build_.  This error was coming much earlier, merely when parsing and examining the request to see if it needed to be handled at all.

I have now added similar defensive error handling around that portion of the code.

Reviewed-on: cerc-io/stack-orchestrator#922
Reviewed-by: zramsay <zramsay@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-14 18:04:31 +00:00
8576137557 Convert port to string. (#919)
The str type check doesn't work if the port is a ruamel.yaml.scalarstring.SingleQuotedScalarString or ruamel.yaml.scalarstring.DoubleQuotedScalarString

Reviewed-on: cerc-io/stack-orchestrator#919
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-14 00:25:35 +00:00
65c1cdf6b1 Fix crash if port has int type in yaml (#918)
Reviewed-on: cerc-io/stack-orchestrator#918
Reviewed-by: Thomas E Lackey <telackey@noreply.git.vdb.to>
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-13 20:47:09 +00:00
265699bc38 Allow to disable kind cluster management for testing (#915)
Reviewed-on: cerc-io/stack-orchestrator#915
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-13 17:48:14 +00:00
4a7670a5d6 Open the json-rpc port (#916)
Reviewed-on: cerc-io/stack-orchestrator#916
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-13 17:47:56 +00:00
6087e1cd31 Copy config under a volume for Docker (similar to a ConfigMap for K8S). (#914)
This emulates the K8S ConfigMap behavior on Docker by using a regular volume.

If a directory exists under `config/` which matches a named volume, the contents will be copied to the volume on `create` (provided the destination volume is empty).  That is, rather than a ConfigMap, it is essentially a "config volume".

For example, with a compose file like:

```
version: '3.7'
services:
  caddy:
    image: cerc/caddy-ethcache:local
    restart: always
    volumes:
      - caddyconfig:/etc/caddy:ro
volumes:
  caddyconfig:
```

And a directory:

```
❯ ls stack-orchestrator/config/caddyconfig/
Caddyfile
```

After `laconic-so deploy create --spec-file caddy.yml --deployment-dir /srv/caddy` there will be:

```
❯ ls /srv/caddy/data/caddyconfig
Caddyfile
```

Mounted at `/etc/caddy`

Reviewed-on: cerc-io/stack-orchestrator#914
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-10 02:32:21 +00:00
1def279d26 Support multiple NodePorts, static NodePort mapping, and add 'replicas' spec option (#913)
NodePort example:

```
network:
  ports:
    caddy:
     - 1234
     - 32020:2020
```

Replicas example:

```
replicas: 2
```

This also adds an optimization for k8s where if a directory matching the name of a configmap exists in beneath config/ in the stack, its contents will be copied into the corresponding configmap.

For example:

```
# Config files in the stack
❯ ls stack-orchestrator/config/caddyconfig
Caddyfile  Caddyfile.one-req-per-upstream-example

# ConfigMap in the spec
❯ cat foo.yml | grep config
...
configmaps:
  caddyconfig: ./configmaps/caddyconfig

# Create the deployment
❯ laconic-so --stack ~/cerc/caddy-ethcache/stack-orchestrator/stacks/caddy-ethcache deploy create --spec-file foo.yml

# The files from beneath config/<config_map_name> have been copied to the ConfigMap directory from the spec.
❯ ls deployment-001/configmaps/caddyconfig
Caddyfile  Caddyfile.one-req-per-upstream-example
```

Reviewed-on: cerc-io/stack-orchestrator#913
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-08-09 02:32:06 +00:00
64691bd206 Merge pull request 'Allow gentx-files to be omitted' (#911) from dboreham/allow-zero-gentx into main
Reviewed-on: cerc-io/stack-orchestrator#911
2024-08-07 20:13:40 +00:00
aef5986135 Allow gentx-files to be omitted 2024-08-07 14:11:06 -06:00
6f8f0340d3 Merge pull request 'Add stage 1 support' (#910) from dboreham/stage1-support into main
Reviewed-on: cerc-io/stack-orchestrator#910
2024-08-07 17:44:28 +00:00
7590d6e237 Add stage 1 support 2024-08-07 11:28:10 -06:00
573f99dbbe Listen on 0.0.0.0 (#909)
Reviewed-on: cerc-io/stack-orchestrator#909
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-08-02 14:06:06 +00:00
8052c1c25e Merge pull request 'Laconicd needs to be told its currency' (#908) from dboreham/mainnet-laconic-specify-currency into main
Reviewed-on: cerc-io/stack-orchestrator#908
2024-08-02 03:10:34 +00:00
a674d13493 Laconicd needs to be told its currency 2024-08-01 21:09:30 -06:00
0d4f4509c8 Remove Eth fixturenet workflows (#906)
Deletes the now-failing CI workflows for the old `fixturenet-eth` and `fixturenet-plugeth` stacks.

Part of cerc-io/stack-orchestrator#905.

Reviewed-on: cerc-io/stack-orchestrator#906
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
2024-08-01 02:28:05 +00:00
5af27b1b3a Merge pull request 'Fix for sh as shell not bash' (#907) from dboreham/fix-script-for-ubuntu into main
Reviewed-on: cerc-io/stack-orchestrator#907
2024-07-31 20:39:37 +00:00
6c91b87348 Fix for sh as shell not bash 2024-07-31 14:30:53 -06:00
7d18334953 Mainnet-laconic stack fixes for laconicd2 (#904)
Reviewed-on: cerc-io/stack-orchestrator#904
2024-07-31 13:51:28 +00:00
79c1c5ed99 Update fixturenet-laconicd stack to use alnt denom (#902)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Reviewed-on: cerc-io/stack-orchestrator#902
Reviewed-by: ashwin <ashwin@noreply.git.vdb.to>
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2024-07-31 13:27:54 +00:00
dfedd9e9ff rename laconic-sdk to registry-sdk (#897)
Co-authored-by: zramsay <zach@bluecollarcoding.ca>
Reviewed-on: cerc-io/stack-orchestrator#897
Reviewed-by: ashwin <ashwin@noreply.git.vdb.to>
Co-authored-by: zramsay <zramsay@noreply.git.vdb.to>
Co-committed-by: zramsay <zramsay@noreply.git.vdb.to>
2024-07-31 08:01:02 +00:00
f64683f03b Use a more flexible mechanism to inject config into next.config.js for runtime env. (#901)
Instead of attempting to rewriting the nextConfig file directly, inject a helper function to add the config we need.

Reviewed-on: cerc-io/stack-orchestrator#901
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-07-31 03:22:23 +00:00
913c3a8557 Back to v2 now that we have a working webapp deployer build again. (#896)
Reviewed-on: cerc-io/stack-orchestrator#896
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-07-27 18:59:42 +00:00
2f5b0cdd13 Revert recent laconicd deployment changes to restore production webapp deployer function. (#895)
Reviewed-on: cerc-io/stack-orchestrator#895
2024-07-27 17:04:03 +00:00
432bd4113d 881: Support next.config.mjs (#890)
Reviewed-on: cerc-io/stack-orchestrator#890
2024-07-25 16:47:17 +00:00
b26698b756 Update fixturenet-laconicd stack for renaming changes (#891)
Part of [Rename laconic2d to laconicd](https://www.notion.so/Rename-laconic2d-to-laconicd-9028d0c020d24d1288e92ebcb773d7a7)
Handles cerc-io/stack-orchestrator#882, cerc-io/stack-orchestrator#889

Reviewed-on: cerc-io/stack-orchestrator#891
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2024-07-25 08:50:15 +00:00
01deac78c4 880: Support new compile/generate syntax for next >=14.2.0 (#886)
Fix for cerc-io/stack-orchestrator#880 to support the next compile/generate syntax.

Reviewed-on: cerc-io/stack-orchestrator#886
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-07-24 20:20:16 +00:00
40ccd47857 Merge pull request 'fixes for deployer & SP documentation' (#887) from zach/cns-to-registry into main
Reviewed-on: cerc-io/stack-orchestrator#887
2024-07-24 00:29:14 +00:00
zramsay
80cff73344 crn --> lrn 2024-07-23 20:20:01 -04:00
zramsay
21b1270d27 fix lint 2024-07-23 20:16:16 -04:00
zramsay
008389dcd8 cns --> registry and other fixes 2024-07-23 20:10:06 -04:00
c81fb9581a Fix stack path check (#877)
Reviewed-on: cerc-io/stack-orchestrator#877
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-19 17:16:40 +00:00
83397bbae4 Enable cors in laconicd http services (#875)
Reviewed-on: cerc-io/stack-orchestrator#875
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-15 05:23:18 +00:00
17c21464ab Merge pull request 'Work around explorer host name sensitivity' (#874) from dboreham/fix-explorer-testnet-hostname into main
Reviewed-on: cerc-io/stack-orchestrator#874
2024-07-15 02:56:33 +00:00
7fb9ccdfd8 Work around explorer host name sensitivity 2024-07-14 20:51:11 -06:00
2bad59dfcd Add missing file to explorer container (#873)
Reviewed-on: cerc-io/stack-orchestrator#873
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-14 17:40:35 +00:00
13d04e9256 Integrate ping-pub explorer (#872)
Reviewed-on: cerc-io/stack-orchestrator#872
2024-07-13 14:24:23 +00:00
1f017c9066 increase CERC_MAX_GENERATE_TIME for webapps (#857)
sort of addresses #856

Co-authored-by: zramsay <zach@bluecollarcoding.ca>
Co-authored-by: David Boreham <dboreham@noreply.git.vdb.to>
Reviewed-on: cerc-io/stack-orchestrator#857
Co-authored-by: zramsay <zramsay@noreply.git.vdb.to>
Co-committed-by: zramsay <zramsay@noreply.git.vdb.to>
2024-07-12 19:01:35 +00:00
3b9422095c Add support for bun as a webapp package manager (#800)
This is working off pull request "[Add support for pnpm as a webapp build tool. #767](https://git.vdb.to/cerc-io/stack-orchestrator/pulls/767/files)" that adds `pnpm` package manager support for `nextjs` & `webapps`.

`bun` default build output directory (defined as `CERC_BUILD_OUTPUT_DIR`) is `dist` which should already be handled with `pnpm` support in the previously mentioned [pull request](https://git.vdb.to/cerc-io/stack-orchestrator/pulls/767/files)

Installing `bun` using `npm` following our previous `pnpm` installation

```zsh
npm install -g bun
```

We'll be using `bun` as a package manager that works with `Node.js` projects as defined in bun's [docs](https://bun.sh/docs/cli/install)

> The bun CLI contains a Node.js-compatible package manager designed to be a dramatically faster replacement for npm, yarn, and pnpm. It's a standalone tool that will work in pre-existing Node.js projects; if your project has a package.json, bun install can help you speed up your workflow.

To test `next.js` apps using `node.js` and compatibility with all four packager managers -- `npm`, `yarn`, `pnpm`, and `bun` -- use the branches of snowball's [nextjs-package-manager-example-app](https://git.vdb.to/snowball/nextjs-package-manager-example-app) repo: `nextjs-package-manager/npm`, `nextjs-package-manager/yarn`, `nextjs-package-manager/pnpm`, `nextjs-package-manager/bun`.

Co-authored-by: Vivian Phung <dev+github@vivianphung.com>
Co-authored-by: David Boreham <dboreham@noreply.git.vdb.to>
Reviewed-on: https://git.vdb.to/cerc-io/stack-orchestrator/pulls/800
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: VPhung24 <vphung24@noreply.git.vdb.to>
Co-committed-by: VPhung24 <vphung24@noreply.git.vdb.to>
2024-07-09 18:00:14 +00:00
36d4969b2d Fixes for external stack deployment (#851)
Fixes
- stack path resolution for `build`
- external stack path resolution for deployments
- "extra" config detection
- `deployment ports` command
- `version` command in dist or source install (without build_tag.txt)
- `setup-repos`, so it won't die when an existing repo is not at a branch or exact tag

Used in cerc-io/fixturenet-eth-stacks#14

Reviewed-on: cerc-io/stack-orchestrator#851
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
2024-07-09 15:37:35 +00:00
a2d6201be9 Merge pull request 'Remove quotes from git config' (#870) from dboreham/fix-git-config-command into main
Reviewed-on: cerc-io/stack-orchestrator#870
2024-07-05 15:56:12 +00:00
62f7825ec2 Remove quotes from git config 2024-07-05 09:55:14 -06:00
6d24d4a7e6 Set github auth token if present (#868)
Reviewed-on: cerc-io/stack-orchestrator#868
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-05 12:27:22 +00:00
f06e5f9a2a Don't try to tag remote images (#866)
Reviewed-on: cerc-io/stack-orchestrator#866
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-04 23:51:06 +00:00
c3a1402042 Derive the webapp host container id from stable data (#865)
Reviewed-on: cerc-io/stack-orchestrator#865
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-04 22:38:07 +00:00
ca5fffaed5 Add logging to webapp deployer (#863)
Helps with diagnosing failures and odd behavior seen in the deployer in production.

Reviewed-on: cerc-io/stack-orchestrator#863
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
2024-07-04 19:46:42 +00:00
df776c1b4c Install git for apps that want to get their commit sha (#854)
Reviewed-on: cerc-io/stack-orchestrator#854
Reviewed-by: Thomas E Lackey <telackey@noreply.git.vdb.to>
Co-authored-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-committed-by: David Boreham <dboreham@noreply.git.vdb.to>
2024-06-25 05:03:49 +00:00
48a3e79e6a Merge pull request 'Fix argument errors in command code' (#853) from dboreham/fix-laconic-mainnet-command into main
Reviewed-on: cerc-io/stack-orchestrator#853
2024-06-24 20:21:08 +00:00
0eaa5b8f09 Fix argument errors in command code 2024-06-24 14:15:46 -06:00
8980ac2581 Merge pull request 'Fixes for current SO objects' (#852) from dboreham/fix-mainnet-laconic into main
Reviewed-on: cerc-io/stack-orchestrator#852
2024-06-24 19:54:04 +00:00
fd15252c3f Fixes for current SO objects 2024-06-24 13:41:15 -06:00
79 changed files with 2508 additions and 941 deletions

View File

@ -1,57 +0,0 @@
name: Fixturenet-Eth-Plugeth-Test
on:
push:
branches: '*'
paths:
- '!**'
- '.gitea/workflows/triggers/fixturenet-eth-plugeth-test'
schedule: # Note: coordinate with other tests to not overload runners at the same time of day
- cron: '2 14 * * *'
jobs:
test:
name: "Run an Ethereum plugeth fixturenet test"
runs-on: ubuntu-latest
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
# At present the stock setup-python action fails on Linux/aarch64
# Conditional steps below workaroud this by using deadsnakes for that case only
- name: "Install Python for ARM on Linux"
if: ${{ runner.arch == 'arm64' && runner.os == 'Linux' }}
uses: deadsnakes/action@v3.0.1
with:
python-version: '3.8'
- name: "Install Python cases other than ARM on Linux"
if: ${{ ! (runner.arch == 'arm64' && runner.os == 'Linux') }}
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
run: python3 --version
- name: "Install shiv"
run: pip install shiv
- name: "Generate build version file"
run: ./scripts/create_build_tag_file.sh
- name: "Build local shiv package"
run: ./scripts/build_shiv_package.sh
- name: "Run fixturenet-eth tests"
run: ./tests/fixturenet-eth-plugeth/run-test.sh
- name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS }}
- name: Notify DeepStack Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS }}

View File

@ -1,55 +0,0 @@
name: Fixturenet-Eth-Test
on:
push:
branches: '*'
paths:
- '!**'
- '.gitea/workflows/triggers/fixturenet-eth-test'
jobs:
test:
name: "Run an Ethereum fixturenet test"
runs-on: ubuntu-latest
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
# At present the stock setup-python action fails on Linux/aarch64
# Conditional steps below workaroud this by using deadsnakes for that case only
- name: "Install Python for ARM on Linux"
if: ${{ runner.arch == 'arm64' && runner.os == 'Linux' }}
uses: deadsnakes/action@v3.0.1
with:
python-version: '3.8'
- name: "Install Python cases other than ARM on Linux"
if: ${{ ! (runner.arch == 'arm64' && runner.os == 'Linux') }}
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
run: python3 --version
- name: "Install shiv"
run: pip install shiv
- name: "Generate build version file"
run: ./scripts/create_build_tag_file.sh
- name: "Build local shiv package"
run: ./scripts/build_shiv_package.sh
- name: "Run fixturenet-eth tests"
run: ./tests/fixturenet-eth/run-test.sh
- name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS }}
- name: Notify DeepStack Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS }}

View File

@ -1,19 +1,22 @@
name: Fixturenet-Eth-Plugeth-Arm-Test name: K8s Deployment Control Test
on: on:
pull_request:
branches: '*'
push: push:
branches: '*' branches: '*'
paths: paths:
- '!**' - '!**'
- '.gitea/workflows/triggers/fixturenet-eth-plugeth-arm-test' - '.gitea/workflows/triggers/test-k8s-deployment-control'
- '.gitea/workflows/test-k8s-deployment-control.yml'
- 'tests/k8s-deployment-control/run-test.sh'
schedule: # Note: coordinate with other tests to not overload runners at the same time of day schedule: # Note: coordinate with other tests to not overload runners at the same time of day
- cron: '2 14 * * *' - cron: '3 30 * * *'
jobs: jobs:
test: test:
name: "Run an Ethereum plugeth fixturenet test" name: "Run deployment control suite on kind/k8s"
runs-on: ubuntu-latest-arm runs-on: ubuntu-22.04
steps: steps:
- name: "Clone project repository" - name: "Clone project repository"
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -37,8 +40,17 @@ jobs:
run: ./scripts/create_build_tag_file.sh run: ./scripts/create_build_tag_file.sh
- name: "Build local shiv package" - name: "Build local shiv package"
run: ./scripts/build_shiv_package.sh run: ./scripts/build_shiv_package.sh
- name: "Run fixturenet-eth tests" - name: "Check cgroups version"
run: ./tests/fixturenet-eth-plugeth/run-test.sh run: mount | grep cgroup
- name: "Install kind"
run: ./tests/scripts/install-kind.sh
- name: "Install Kubectl"
run: ./tests/scripts/install-kubectl.sh
- name: "Run k8s deployment control test"
run: |
source /opt/bash-utils/cgroup-helper.sh
join_cgroup
./tests/k8s-deployment-control/run-test.sh
- name: Notify Vulcanize Slack on CI failure - name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }} if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2 uses: ravsamhq/notify-slack-action@v2

View File

@ -1,2 +0,0 @@
Change this file to trigger running the fixturenet-eth-plugeth-arm-test CI job

View File

@ -1,3 +0,0 @@
Change this file to trigger running the fixturenet-eth-plugeth-test CI job
trigger
trigger

View File

@ -1,2 +0,0 @@
Change this file to trigger running the fixturenet-eth-test CI job

View File

@ -5,3 +5,6 @@ Trigger
Trigger Trigger
Trigger Trigger
Trigger Trigger
Trigger
Trigger
Trigger

View File

@ -51,7 +51,7 @@ $ laconic-so build-npms --include <package-name>
``` ```
e.g. e.g.
``` ```
$ laconic-so build-npms --include laconic-sdk $ laconic-so build-npms --include registry-sdk
``` ```
Build the packages for a stack: Build the packages for a stack:
``` ```

View File

@ -56,7 +56,7 @@ laconic-so --stack fixturenet-laconicd build-npms
Navigate to the Gitea console and switch to the `cerc-io` user then find the `Packages` tab to confirm that these two npm packages have been published: Navigate to the Gitea console and switch to the `cerc-io` user then find the `Packages` tab to confirm that these two npm packages have been published:
- `@cerc-io/laconic-registry-cli` - `@cerc-io/laconic-registry-cli`
- `@cerc-io/laconic-sdk` - `@cerc-io/registry-sdk`
### Build and deploy fixturenet containers ### Build and deploy fixturenet containers
@ -74,7 +74,7 @@ laconic-so --stack fixturenet-laconicd deploy logs
### Test with the registry CLI ### Test with the registry CLI
```bash ```bash
laconic-so --stack fixturenet-laconicd deploy exec cli "laconic cns status" laconic-so --stack fixturenet-laconicd deploy exec cli "laconic registry status"
``` ```
Try additional CLI commands, documented [here](https://github.com/cerc-io/laconic-registry-cli#operations). Try additional CLI commands, documented [here](https://github.com/cerc-io/laconic-registry-cli#operations).

View File

@ -0,0 +1,27 @@
# K8S Deployment Enhancements
## Controlling pod placement
The placement of pods created as part of a stack deployment can be controlled to either avoid certain nodes, or require certain nodes.
### Pod/Node Affinity
Node affinity rules applied to pods target node labels. The effect is that a pod can only be placed on a node having the specified label value. Note that other pods that do not have any node affinity rules can also be placed on those same nodes. Thus node affinity for a pod controls where that pod can be placed, but does not control where other pods are placed.
Node affinity for stack pods is specified in the deployment's `spec.yml` file as follows:
```
node-affinities:
- label: nodetype
value: typeb
```
This example denotes that the stack's pods should only be placed on nodes that have the label `nodetype` with value `typeb`.
### Node Taint Toleration
K8s nodes can be given one or more "taints". These are special fields (distinct from labels) with a name (key) and optional value.
When placing pods, the k8s scheduler will only assign a pod to a tainted node if the pod posesses a corresponding "toleration".
This is metadata associated with the pod that specifies that the pod "tolerates" a given taint.
Therefore taint toleration provides a mechanism by which only certain pods can be placed on specific nodes, and provides a complementary mechanism to node affinity.
Taint toleration for stack pods is specified in the deployment's `spec.yml` file as follows:
```
node-tolerations:
- key: nodetype
value: typeb
```
This example denotes that the stack's pods will tolerate a taint: `nodetype=typeb`

View File

@ -1,9 +1,8 @@
# Running a laconicd fixturenet with console # Running a laconicd fixturenet with console
The following tutorial explains the steps to run a laconicd fixturenet with CLI and web console that displays records in the registry. It is designed as an introduction to Stack Orchestrator and to showcase one component of the Laconic Stack. Prior to Stack Orchestrator, the following 4 repositories had to be cloned and setup manually: The following tutorial explains the steps to run a laconicd fixturenet with CLI and web console that displays records in the registry. It is designed as an introduction to Stack Orchestrator and to showcase one component of the Laconic Stack. Prior to Stack Orchestrator, the following repositories had to be cloned and setup manually:
- https://git.vdb.to/cerc-io/laconicd - https://git.vdb.to/cerc-io/laconicd
- https://git.vdb.to/cerc-io/laconic-sdk
- https://git.vdb.to/cerc-io/laconic-registry-cli - https://git.vdb.to/cerc-io/laconic-registry-cli
- https://git.vdb.to/cerc-io/laconic-console - https://git.vdb.to/cerc-io/laconic-console
@ -51,7 +50,7 @@ To avoid hiccups on Mac M1/M2 and any local machine nuances that may affect the
1. Get the repositories 1. Get the repositories
``` ```
laconic-so --stack fixturenet-laconic-loaded setup-repositories --include git.vdb.to/cerc-io/laconicd,git.vdb.to/cerc-io/laconic-sdk,git.vdb.to/cerc-io/laconic-registry-cli,git.vdb.to/cerc-io/laconic-console laconic-so --stack fixturenet-laconic-loaded setup-repositories --include git.vdb.to/cerc-io/laconicd
``` ```
1. Build the containers: 1. Build the containers:
@ -76,6 +75,8 @@ To avoid hiccups on Mac M1/M2 and any local machine nuances that may affect the
1. Create a deployment directory for the stack: 1. Create a deployment directory for the stack:
``` ```
laconic-so --stack fixturenet-laconic-loaded deploy init --output laconic-loaded.spec --map-ports-to-host any-same --config LACONIC_HOSTED_ENDPOINT=$BACKEND_ENDPOINT laconic-so --stack fixturenet-laconic-loaded deploy init --output laconic-loaded.spec --map-ports-to-host any-same --config LACONIC_HOSTED_ENDPOINT=$BACKEND_ENDPOINT
# Update port mapping in the laconic-loaded.spec file to resolve port conflicts on host if any
``` ```
``` ```
laconic-so --stack fixturenet-laconic-loaded deploy create --deployment-dir laconic-loaded-deployment --spec-file laconic-loaded.spec laconic-so --stack fixturenet-laconic-loaded deploy create --deployment-dir laconic-loaded-deployment --spec-file laconic-loaded.spec
@ -95,52 +96,51 @@ To avoid hiccups on Mac M1/M2 and any local machine nuances that may affect the
You'll see output from `laconicd` and the block height should be >1 to confirm it is running: You'll see output from `laconicd` and the block height should be >1 to confirm it is running:
``` ```
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:29PM INF indexed block exents height=12 module=txindex server=node laconicd-1 | 6:12AM INF indexed block events height=16 module=txindex
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF Timed out dur=4976.960115 height=13 module=consensus round=0 server=node step=1 laconicd-1 | 6:12AM INF Timed out dur=2993.893332 height=17 module=consensus round=0 step=RoundStepNewHeight
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF received proposal module=consensus proposal={"Type":32,"block_id":{"hash":"D26C088A711F912ADB97888C269F628DA33153795621967BE44DCB43C3D03CA4","parts":{"hash":"22411A20B7F14CDA33244420FBDDAF24450C0628C7A06034FF22DAC3699DDCC8","total":1}},"height":13,"pol_round":-1,"round":0,"signature":"DEuqnaQmvyYbUwckttJmgKdpRu6eVm9i+9rQ1pIrV2PidkMNdWRZBLdmNghkIrUzGbW8Xd7UVJxtLRmwRASgBg==","timestamp":"2023-04-18T21:30:01.49450663Z"} server=node laconicd-1 | 6:12AM INF received proposal module=consensus proposal="Proposal{17/0 (E15D03C180CE607AE8340A1325A0C134DFB4E1ADD992E173C701EBD362523267:1:DF138772FEF0, -1) 6A6F3B0A42B3 @ 2024-07-25T06:12:31.952967053Z}" proposer=86970D950BC9C16F3991A52D9C6DC55BA478A7C6
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF received complete proposal block hash=D26C088A711F912ADB97888C269F628DA33153795621967BE44DCB43C3D03CA4 height=13 module=consensus server=node laconicd-1 | 6:12AM INF received complete proposal block hash=E15D03C180CE607AE8340A1325A0C134DFB4E1ADD992E173C701EBD362523267 height=17 module=consensus
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF finalizing commit of block hash={} height=13 module=consensus num_txs=0 root=1A8CA1AF139CCC80EC007C6321D8A63A46A793386EE2EDF9A5CA0AB2C90728B7 server=node laconicd-1 | 6:12AM INF finalizing commit of block hash=E15D03C180CE607AE8340A1325A0C134DFB4E1ADD992E173C701EBD362523267 height=17 module=consensus num_txs=0 root=AF4941107DC718ED1425E77A3DC7F1154FB780B7A7DE20288DC43442203527E3
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF minted coins from module account amount=2059730459416582643aphoton from=mint module=x/bank laconicd-1 | 6:12AM INF finalized block block_app_hash=26A665360BB1EE64E54F97F2A5AB7F621B33A86D9896574000C05DE63F43F788 height=17 module=state num_txs_res=0 num_val_updates=0
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF executed block height=13 module=state num_invalid_txs=0 num_valid_txs=0 server=node laconicd-1 | 6:12AM INF executed block app_hash=26A665360BB1EE64E54F97F2A5AB7F621B33A86D9896574000C05DE63F43F788 height=17 module=state
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF commit synced commit=436F6D6D697449447B5B363520313037203630203232372039352038352032303820313334203231392032303520313433203130372031343920313431203139203139322038362031323720362031383520323533203137362031333820313735203135392031383620323334203135382031323120313431203230342037335D3A447D laconicd-1 | 6:12AM INF committed state block_app_hash=AF4941107DC718ED1425E77A3DC7F1154FB780B7A7DE20288DC43442203527E3 height=17 module=state
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF committed state app_hash=416B3CE35F55D086DBCD8F6B958D13C0567F06B9FDB08AAF9FBAEA9E798DCC49 height=13 module=state num_txs=0 server=node laconicd-1 | 6:12AM INF indexed block events height=17 module=txindex
laconic-5cd0a80c1442c3044c8b295d26426bae-laconicd-1 | 9:30PM INF indexed block exents height=13 module=txindex server=node
``` ```
4. Confirm operation of the registry CLI: 4. Confirm operation of the registry CLI:
``` ```
laconic-so deployment --dir laconic-loaded-deployment exec cli "laconic cns status" laconic-so deployment --dir laconic-loaded-deployment exec cli "laconic registry status"
``` ```
``` ```
{ {
"version": "0.3.0", "version": "0.3.0",
"node": { "node": {
"id": "4216af2ac9f68bda33a38803fc1b5c9559312c1d", "id": "6e072894aa1f5d9535a1127a0d7a7f8e65100a2c",
"network": "laconic_9000-1", "network": "laconic_9000-1",
"moniker": "localtestnet" "moniker": "localtestnet"
}, },
"sync": { "sync": {
"latest_block_hash": "1BDF4CB9AE2390DA65BCF997C83133C18014FCDDCAE03708488F0B56FCEEA429", "latestBlockHash": "260102C283D0411CFBA0270F7DC182650FFCA737A2F6F652A985F6065696F590",
"latest_block_height": "5", "latestBlockHeight": "49",
"latest_block_time": "2023-08-09 16:00:30.386903172 +0000 UTC", "latestBlockTime": "2024-07-25 06:14:05.626744215 +0000 UTC",
"catching_up": false "catchingUp": false
}, },
"validator": { "validator": {
"address": "651FBC700B747C76E90ACFC18CC9508C3D0905B9", "address": "86970D950BC9C16F3991A52D9C6DC55BA478A7C6",
"voting_power": "1000000000000000" "votingPower": "1000000000000000"
}, },
"validators": [ "validators": [
{ {
"address": "651FBC700B747C76E90ACFC18CC9508C3D0905B9", "address": "86970D950BC9C16F3991A52D9C6DC55BA478A7C6",
"voting_power": "1000000000000000", "votingPower": "1000000000000000",
"proposer_priority": "0" "proposerPriority": "0"
} }
], ],
"num_peers": "0", "numPeers": "0",
"peers": [], "peers": [],
"disk_usage": "292.0K" "diskUsage": "688K"
} }
``` ```
@ -186,13 +186,13 @@ wns
1. The following command will create a bond and publish a record: 1. The following command will create a bond and publish a record:
``` ```
laconic-so --stack fixturenet-laconic-loaded deploy exec cli ./scripts/create-demo-records.sh laconic-so deployment --dir laconic-loaded-deployment exec cli ./scripts/create-demo-records.sh
``` ```
You'll get an output like: You'll get an output like:
``` ```
Balance is: 99998999999999998999600000 Balance is: 9.9999e+25
Created bond with id: dd88e8d6f9567b32b28e70552aea4419c5dd3307ebae85a284d1fe38904e301a Created bond with id: dd88e8d6f9567b32b28e70552aea4419c5dd3307ebae85a284d1fe38904e301a
Published demo-record-1.yml with id: bafyreierh3xnfivexlscdwubvczmddsnf46uytyfvrbdhkjzztvsz6ruly Published demo-record-1.yml with id: bafyreierh3xnfivexlscdwubvczmddsnf46uytyfvrbdhkjzztvsz6ruly
``` ```
@ -223,5 +223,5 @@ record:
- e.g,: - e.g,:
``` ```
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns record list" laconic-so deployment --dir laconic-loaded-deployment exec cli "laconic registry record list"
``` ```

View File

@ -11,3 +11,5 @@ tomli==2.0.1
validators==0.22.0 validators==0.22.0
kubernetes>=28.1.0 kubernetes>=28.1.0
humanfriendly>=10.0 humanfriendly>=10.0
python-gnupg>=0.5.2
requests>=2.3.2

View File

@ -4,9 +4,11 @@ with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
with open("requirements.txt", "r", encoding="utf-8") as fh: with open("requirements.txt", "r", encoding="utf-8") as fh:
requirements = fh.read() requirements = fh.read()
with open("stack_orchestrator/data/version.txt", "r", encoding="utf-8") as fh:
version = fh.readlines()[-1].strip(" \n")
setup( setup(
name='laconic-stack-orchestrator', name='laconic-stack-orchestrator',
version='1.0.12', version=version,
author='Cerc', author='Cerc',
author_email='info@cerc.io', author_email='info@cerc.io',
license='GNU Affero General Public License', license='GNU Affero General Public License',

View File

@ -21,11 +21,6 @@ from stack_orchestrator.util import get_parsed_stack_config, warn_exit
def get_containers_in_scope(stack: str): def get_containers_in_scope(stack: str):
# See: https://stackoverflow.com/a/20885799/1701505
from stack_orchestrator import data
with importlib.resources.open_text(data, "container-image-list.txt") as container_list_file:
all_containers = container_list_file.read().splitlines()
containers_in_scope = [] containers_in_scope = []
if stack: if stack:
stack_config = get_parsed_stack_config(stack) stack_config = get_parsed_stack_config(stack)
@ -33,7 +28,10 @@ def get_containers_in_scope(stack: str):
warn_exit(f"stack {stack} does not define any containers") warn_exit(f"stack {stack} does not define any containers")
containers_in_scope = stack_config['containers'] containers_in_scope = stack_config['containers']
else: else:
containers_in_scope = all_containers # See: https://stackoverflow.com/a/20885799/1701505
from stack_orchestrator import data
with importlib.resources.open_text(data, "container-image-list.txt") as container_list_file:
containers_in_scope = container_list_file.read().splitlines()
if opts.o.verbose: if opts.o.verbose:
print(f'Containers: {containers_in_scope}') print(f'Containers: {containers_in_scope}')

View File

@ -30,6 +30,12 @@ from stack_orchestrator.build import build_containers
from stack_orchestrator.deploy.webapp.util import determine_base_container, TimedLogger from stack_orchestrator.deploy.webapp.util import determine_base_container, TimedLogger
from stack_orchestrator.build.build_types import BuildContext from stack_orchestrator.build.build_types import BuildContext
def create_env_file(env_vars, repo_root):
env_file_path = os.path.join(repo_root, '.env')
with open(env_file_path, 'w') as env_file:
for key, value in env_vars.items():
env_file.write(f"{key}={value}\n")
return env_file_path
@click.command() @click.command()
@click.option('--base-container') @click.option('--base-container')
@ -37,8 +43,9 @@ from stack_orchestrator.build.build_types import BuildContext
@click.option("--force-rebuild", is_flag=True, default=False, help="Override dependency checking -- always rebuild") @click.option("--force-rebuild", is_flag=True, default=False, help="Override dependency checking -- always rebuild")
@click.option("--extra-build-args", help="Supply extra arguments to build") @click.option("--extra-build-args", help="Supply extra arguments to build")
@click.option("--tag", help="Container tag (default: cerc/<app_name>:local)") @click.option("--tag", help="Container tag (default: cerc/<app_name>:local)")
@click.option("--env", help="Environment variables for webapp (format: KEY1=VALUE1,KEY2=VALUE2)", default="")
@click.pass_context @click.pass_context
def command(ctx, base_container, source_repo, force_rebuild, extra_build_args, tag): def command(ctx, base_container, source_repo, force_rebuild, extra_build_args, tag, env):
'''build the specified webapp container''' '''build the specified webapp container'''
logger = TimedLogger() logger = TimedLogger()
@ -88,9 +95,28 @@ def command(ctx, base_container, source_repo, force_rebuild, extra_build_args, t
# Now build the target webapp. We use the same build script, but with a different Dockerfile and work dir. # Now build the target webapp. We use the same build script, but with a different Dockerfile and work dir.
container_build_env["CERC_WEBAPP_BUILD_RUNNING"] = "true" container_build_env["CERC_WEBAPP_BUILD_RUNNING"] = "true"
container_build_env["CERC_CONTAINER_BUILD_WORK_DIR"] = os.path.abspath(source_repo) container_build_env["CERC_CONTAINER_BUILD_WORK_DIR"] = os.path.abspath(source_repo)
container_build_env["CERC_CONTAINER_BUILD_DOCKERFILE"] = os.path.join(container_build_dir,
# Check if Dockerfile exists in the repository
repo_dockerfile = os.path.join(container_build_env["CERC_CONTAINER_BUILD_WORK_DIR"], "Dockerfile")
default_dockerfile = os.path.join(container_build_dir,
base_container.replace("/", "-"), base_container.replace("/", "-"),
"Dockerfile.webapp") "Dockerfile.webapp")
if os.path.isfile(repo_dockerfile):
env_vars = {}
if env:
for pair in env.split(','):
key, value = pair.split('=')
env_vars[key.strip()] = value.strip()
container_build_env["CERC_CONTAINER_BUILD_DOCKERFILE"] = repo_dockerfile
# Create .env file with environment variables
env_file_path = create_env_file(env_vars, container_build_env["CERC_CONTAINER_BUILD_WORK_DIR"])
container_build_env["CERC_CONTAINER_BUILD_ENV_FILE"] = env_file_path
else:
container_build_env["CERC_CONTAINER_BUILD_DOCKERFILE"] = default_dockerfile
if not tag: if not tag:
webapp_name = os.path.abspath(source_repo).split(os.path.sep)[-1] webapp_name = os.path.abspath(source_repo).split(os.path.sep)[-1]
tag = f"cerc/{webapp_name}:local" tag = f"cerc/{webapp_name}:local"

View File

@ -34,5 +34,8 @@ volumes_key = "volumes"
security_key = "security" security_key = "security"
annotations_key = "annotations" annotations_key = "annotations"
labels_key = "labels" labels_key = "labels"
replicas_key = "replicas"
node_affinities_key = "node-affinities"
node_tolerations_key = "node-tolerations"
kind_config_filename = "kind-config.yml" kind_config_filename = "kind-config.yml"
kube_config_filename = "kubeconfig.yml" kube_config_filename = "kubeconfig.yml"

View File

@ -2,10 +2,11 @@ services:
laconicd: laconicd:
restart: unless-stopped restart: unless-stopped
image: cerc/laconicd:local image: cerc/laconicd:local
command: ["sh", "/docker-entrypoint-scripts.d/create-fixturenet.sh"] command: ["bash", "/docker-entrypoint-scripts.d/create-fixturenet.sh"]
environment: environment:
TEST_AUCTION_ENABLED: ${TEST_AUCTION_ENABLED} TEST_AUCTION_ENABLED: ${TEST_AUCTION_ENABLED:-false}
TEST_REGISTRY_EXPIRY: ${TEST_REGISTRY_EXPIRY} TEST_REGISTRY_EXPIRY: ${TEST_REGISTRY_EXPIRY:-false}
ONBOARDING_ENABLED: ${ONBOARDING_ENABLED:-false}
volumes: volumes:
# The cosmos-sdk node's database directory: # The cosmos-sdk node's database directory:
- laconicd-data:/root/.laconicd - laconicd-data:/root/.laconicd
@ -19,11 +20,9 @@ services:
- "26657" - "26657"
- "26656" - "26656"
- "9473" - "9473"
- "8545"
- "8546"
- "9090" - "9090"
- "9091"
- "1317" - "1317"
cli: cli:
image: cerc/laconic-registry-cli:local image: cerc/laconic-registry-cli:local
volumes: volumes:

View File

@ -0,0 +1,10 @@
services:
laconic-explorer:
restart: unless-stopped
image: cerc/ping-pub:local
environment:
- LACONIC_LACONICD_API_URL=${LACONIC_LACONICD_API_URL:-http://localhost:1317}
- LACONIC_LACONICD_RPC_URL=${LACONIC_LACONICD_RPC_URL:-http://localhost:26657}
- LACONIC_LACONICD_CHAIN_ID=${LACONIC_LACONICD_CHAIN_ID:-chain-id-not-set}
ports:
- "5173"

View File

@ -8,68 +8,68 @@ KEY="mykey"
CHAINID="laconic_9000-1" CHAINID="laconic_9000-1"
MONIKER="localtestnet" MONIKER="localtestnet"
KEYRING="test" KEYRING="test"
KEYALGO="eth_secp256k1" KEYALGO="secp256k1"
LOGLEVEL="info" LOGLEVEL="${LOGLEVEL:-info}"
# trace evm DENOM="alnt"
TRACE="--trace"
# TRACE=""
if [ "$1" == "clean" ] || [ ! -d "$HOME/.laconicd/data/blockstore.db" ]; then if [ "$1" == "clean" ] || [ ! -d "$HOME/.laconicd/data/blockstore.db" ]; then
# validate dependencies are installed # validate dependencies are installed
command -v jq > /dev/null 2>&1 || { echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/"; exit 1; } command -v jq > /dev/null 2>&1 || {
echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/"
exit 1
}
# remove existing daemon and client # remove existing daemon and client
rm -rf $HOME/.laconicd/* rm -rf $HOME/.laconicd/*
rm -rf $HOME/.laconic/*
if [ -n "`which make`" ]; then if [ -n "`which make`" ]; then
make install make install
fi fi
laconicd config keyring-backend $KEYRING laconicd config set client chain-id $CHAINID
laconicd config chain-id $CHAINID laconicd config set client keyring-backend $KEYRING
# if $KEY exists it should be deleted # if $KEY exists it should be deleted
laconicd keys add $KEY --keyring-backend $KEYRING --algo $KEYALGO laconicd keys add $KEY --keyring-backend $KEYRING --algo $KEYALGO
# Set moniker and chain-id for Ethermint (Moniker can be anything, chain-id must be an integer) # Set moniker and chain-id for Ethermint (Moniker can be anything, chain-id must be an integer)
laconicd init $MONIKER --chain-id $CHAINID laconicd init $MONIKER --chain-id $CHAINID --default-denom $DENOM
# Change parameter token denominations to aphoton update_genesis() {
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["staking"]["params"]["bond_denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json jq "$1" $HOME/.laconicd/config/genesis.json > $HOME/.laconicd/config/tmp_genesis.json &&
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["crisis"]["constant_fee"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["gov"]["deposit_params"]["min_deposit"][0]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json }
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["mint"]["params"]["mint_denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
# Custom modules
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["record_rent"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_rent"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_commit_fee"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_reveal_fee"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_minimum_bid"]["denom"]="aphoton"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json
if [[ "$TEST_REGISTRY_EXPIRY" == "true" ]]; then if [[ "$TEST_REGISTRY_EXPIRY" == "true" ]]; then
echo "Setting timers for expiry tests." echo "Setting timers for expiry tests."
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["record_rent_duration"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["record_rent_duration"]="60s"'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_grace_period"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_grace_period"]="60s"'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_rent_duration"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_rent_duration"]="60s"'
fi fi
if [[ "$TEST_AUCTION_ENABLED" == "true" ]]; then if [[ "$TEST_AUCTION_ENABLED" == "true" ]]; then
echo "Enabling auction and setting timers." echo "Enabling auction and setting timers."
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_enabled"]=true' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_auction_enabled"]=true'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_rent_duration"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_rent_duration"]="60s"'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_grace_period"]="300s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_grace_period"]="300s"'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_commits_duration"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_auction_commits_duration"]="60s"'
cat $HOME/.laconicd/config/genesis.json | jq '.app_state["registry"]["params"]["authority_auction_reveals_duration"]="60s"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.app_state["registry"]["params"]["authority_auction_reveals_duration"]="60s"'
fi
if [[ "$ONBOARDING_ENABLED" == "true" ]]; then
echo "Enabling validator onboarding."
update_genesis '.app_state["onboarding"]["params"]["onboarding_enabled"]=true'
fi fi
# increase block time (?) # increase block time (?)
cat $HOME/.laconicd/config/genesis.json | jq '.consensus_params["block"]["time_iota_ms"]="1000"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.consensus["params"]["block"]["time_iota_ms"]="1000"'
# Set gas limit in genesis # Set gas limit in genesis
cat $HOME/.laconicd/config/genesis.json | jq '.consensus_params["block"]["max_gas"]="10000000"' > $HOME/.laconicd/config/tmp_genesis.json && mv $HOME/.laconicd/config/tmp_genesis.json $HOME/.laconicd/config/genesis.json update_genesis '.consensus["params"]["block"]["max_gas"]="10000000"'
# disable produce empty block # disable produce empty block
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
@ -78,30 +78,6 @@ if [ "$1" == "clean" ] || [ ! -d "$HOME/.laconicd/data/blockstore.db" ]; then
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME/.laconicd/config/config.toml sed -i 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME/.laconicd/config/config.toml
fi fi
if [[ $1 == "pending" ]]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' 's/create_empty_blocks_interval = "0s"/create_empty_blocks_interval = "30s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_propose = "3s"/timeout_propose = "30s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_propose_delta = "500ms"/timeout_propose_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_prevote = "1s"/timeout_prevote = "10s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_prevote_delta = "500ms"/timeout_prevote_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_precommit = "1s"/timeout_precommit = "10s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_precommit_delta = "500ms"/timeout_precommit_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_commit = "5s"/timeout_commit = "150s"/g' $HOME/.laconicd/config/config.toml
sed -i '' 's/timeout_broadcast_tx_commit = "10s"/timeout_broadcast_tx_commit = "150s"/g' $HOME/.laconicd/config/config.toml
else
sed -i 's/create_empty_blocks_interval = "0s"/create_empty_blocks_interval = "30s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_propose = "3s"/timeout_propose = "30s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_propose_delta = "500ms"/timeout_propose_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_prevote = "1s"/timeout_prevote = "10s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_prevote_delta = "500ms"/timeout_prevote_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_precommit = "1s"/timeout_precommit = "10s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_precommit_delta = "500ms"/timeout_precommit_delta = "5s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_commit = "5s"/timeout_commit = "150s"/g' $HOME/.laconicd/config/config.toml
sed -i 's/timeout_broadcast_tx_commit = "10s"/timeout_broadcast_tx_commit = "150s"/g' $HOME/.laconicd/config/config.toml
fi
fi
# Enable telemetry (prometheus metrics: http://localhost:1317/metrics?format=prometheus) # Enable telemetry (prometheus metrics: http://localhost:1317/metrics?format=prometheus)
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' 's/enabled = false/enabled = true/g' $HOME/.laconicd/config/app.toml sed -i '' 's/enabled = false/enabled = true/g' $HOME/.laconicd/config/app.toml
@ -114,23 +90,27 @@ if [ "$1" == "clean" ] || [ ! -d "$HOME/.laconicd/data/blockstore.db" ]; then
fi fi
# Allocate genesis accounts (cosmos formatted addresses) # Allocate genesis accounts (cosmos formatted addresses)
laconicd add-genesis-account $KEY 100000000000000000000000000aphoton --keyring-backend $KEYRING # 10^30 alnt | 10^12 lnt
laconicd genesis add-genesis-account $KEY 1000000000000000000000000000000$DENOM --keyring-backend $KEYRING
# Sign genesis transaction # Sign genesis transaction
laconicd gentx $KEY 1000000000000000000000aphoton --keyring-backend $KEYRING --chain-id $CHAINID # 10^24 alnt | 10^6 lnt
laconicd genesis gentx $KEY 1000000000000000000000000$DENOM --keyring-backend $KEYRING --chain-id $CHAINID
# Collect genesis tx # Collect genesis tx
laconicd collect-gentxs laconicd genesis collect-gentxs
# Run this to ensure everything worked and that the genesis file is setup correctly # Run this to ensure everything worked and that the genesis file is setup correctly
laconicd validate-genesis laconicd genesis validate
if [[ $1 == "pending" ]]; then
echo "pending mode is on, please wait for the first block committed."
fi
else else
echo "Using existing database at $HOME/.laconicd. To replace, run '`basename $0` clean'" echo "Using existing database at $HOME/.laconicd. To replace, run '`basename $0` clean'"
fi fi
# Start the node (remove the --pruning=nothing flag if historical queries are not needed) # Start the node (remove the --pruning=nothing flag if historical queries are not needed)
laconicd start --pruning=nothing --evm.tracer=json $TRACE --log_level $LOGLEVEL --minimum-gas-prices=0.0001aphoton --json-rpc.api eth,txpool,personal,net,debug,web3,miner --api.enable --gql-server --gql-playground laconicd start \
--pruning=nothing \
--log_level $LOGLEVEL \
--minimum-gas-prices=1$DENOM \
--api.enable \
--rpc.laddr="tcp://0.0.0.0:26657" \
--gql-server --gql-playground

View File

@ -1,9 +1,9 @@
services: services:
cns: registry:
restEndpoint: 'http://laconicd:1317' rpcEndpoint: 'http://laconicd:26657'
gqlEndpoint: 'http://laconicd:9473/api' gqlEndpoint: 'http://laconicd:9473/api'
userKey: REPLACE_WITH_MYKEY userKey: REPLACE_WITH_MYKEY
bondId: bondId:
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: 350000 gas: 350000
fees: 200000aphoton fees: 2000000alnt

View File

@ -1,9 +1,9 @@
services: services:
cns: registry:
restEndpoint: 'http://laconicd:1317' rpcEndpoint: 'http://laconicd:26657'
gqlEndpoint: 'http://laconicd:9473/api' gqlEndpoint: 'http://laconicd:9473/api'
userKey: REPLACE_WITH_MYKEY userKey: REPLACE_WITH_MYKEY
bondId: bondId:
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: 250000 gas: 250000
fees: 200000aphoton fees: 2000000alnt

View File

@ -1,18 +1,15 @@
#!/bin/sh #!/bin/sh
if [[ -n "$CERC_SCRIPT_DEBUG" ]]; then if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x set -x
fi fi
#TODO: pass these in from the caller #TODO: pass these in from the caller
TRACE="--trace"
LOGLEVEL="info" LOGLEVEL="info"
laconicd start \ laconicd start \
--pruning=nothing \ --pruning=nothing \
--evm.tracer=json $TRACE \
--log_level $LOGLEVEL \ --log_level $LOGLEVEL \
--minimum-gas-prices=0.0001aphoton \ --minimum-gas-prices=1alnt \
--json-rpc.api eth,txpool,personal,net,debug,web3,miner \
--api.enable \ --api.enable \
--gql-server \ --gql-server \
--gql-playground --gql-playground

View File

@ -68,5 +68,5 @@ ENV PATH="${PATH}:/scripts"
COPY entrypoint.sh . COPY entrypoint.sh .
ENTRYPOINT ["./entrypoint.sh"] ENTRYPOINT ["./entrypoint.sh"]
# Placeholder CMD : generally this will be overridden at run time like : # Placeholder CMD : generally this will be overridden at run time like :
# docker run -it -v /home/builder/cerc/laconic-sdk:/workspace cerc/builder-js sh -c 'cd /workspace && yarn && yarn build' # docker run -it -v /home/builder/cerc/registry-sdk:/workspace cerc/builder-js sh -c 'cd /workspace && yarn && yarn build'
CMD node --version CMD node --version

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build cerc/laconic-registry-cli # Build cerc/laconic-console-host
source ${CERC_CONTAINER_BASE_DIR}/build-base.sh source ${CERC_CONTAINER_BASE_DIR}/build-base.sh

View File

@ -5,16 +5,16 @@ if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x set -x
fi fi
registry_command="laconic cns" registry_command="laconic registry"
demo_records_dir="scripts/demo-records" demo_records_dir="scripts/demo-records"
# Check we have funds # Check we have funds
funds_response=$(${registry_command} account get --address $(cat my-address.txt)) funds_response=$(${registry_command} account get --address $(cat my-address.txt))
funds_balance=$(echo ${funds_response} | jq -r .[0].balance[0].quantity) funds_balance=$(echo ${funds_response} | jq -r ".[0].balance[0].quantity")
echo "Balance is: ${funds_balance}" echo "Balance is: ${funds_balance}"
# Create a bond # Create a bond
bond_create_result=$(${registry_command} bond create --type aphoton --quantity 1000000000) bond_create_result=$(${registry_command} bond create --type alnt --quantity 1000000000)
bond_id=$(echo ${bond_create_result} | jq -r .bondId) bond_id=$(echo ${bond_create_result} | jq -r .bondId)
echo "Created bond with id: ${bond_id}" echo "Created bond with id: ${bond_id}"

View File

@ -7,9 +7,9 @@ record:
env: env:
ENV_VAR_A: A ENV_VAR_A: A
ENV_VAR_B: B ENV_VAR_B: B
crn: lrn:
- crn://foo.bar - lrn://foo.bar
- crn://bar.baz - lrn://bar.baz
meta: meta:
foo: bar foo: bar
tags: tags:

View File

@ -28,6 +28,8 @@ RUN \
&& su ${USERNAME} -c "umask 0002 && npm install -g semver" \ && su ${USERNAME} -c "umask 0002 && npm install -g semver" \
# Install pnpm # Install pnpm
&& su ${USERNAME} -c "umask 0002 && npm install -g pnpm" \ && su ${USERNAME} -c "umask 0002 && npm install -g pnpm" \
# Install bun
&& su ${USERNAME} -c "umask 0002 && npm install -g bun@1.1.x" \
&& npm cache clean --force > /dev/null 2>&1 && npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages. # [Optional] Uncomment this section to install additional OS packages.

View File

@ -14,6 +14,8 @@ if [ -z "$CERC_BUILD_TOOL" ]; then
CERC_BUILD_TOOL=pnpm CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else else
CERC_BUILD_TOOL=npm CERC_BUILD_TOOL=npm
fi fi

View File

@ -5,14 +5,19 @@ if [ -n "$CERC_SCRIPT_DEBUG" ]; then
fi fi
CERC_MIN_NEXTVER=13.4.2 CERC_MIN_NEXTVER=13.4.2
CERC_DEFAULT_WEBPACK_VER="5.93.0"
CERC_NEXT_VERSION="${CERC_NEXT_VERSION:-keep}" CERC_NEXT_VERSION="${CERC_NEXT_VERSION:-keep}"
CERC_WEBPACK_VERSION="${CERC_WEBPACK_VERSION:-keep}"
CERC_BUILD_TOOL="${CERC_BUILD_TOOL}" CERC_BUILD_TOOL="${CERC_BUILD_TOOL}"
if [ -z "$CERC_BUILD_TOOL" ]; then if [ -z "$CERC_BUILD_TOOL" ]; then
if [ -f "pnpm-lock.yaml" ]; then if [ -f "pnpm-lock.yaml" ]; then
CERC_BUILD_TOOL=pnpm CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else else
CERC_BUILD_TOOL=npm CERC_BUILD_TOOL=npm
fi fi
@ -23,13 +28,21 @@ WORK_DIR="${1:-/app}"
cd "${WORK_DIR}" || exit 1 cd "${WORK_DIR}" || exit 1
if [ -f "next.config.mjs" ]; then
NEXT_CONFIG_JS="next.config.mjs"
IMPORT_OR_REQUIRE="import"
else
NEXT_CONFIG_JS="next.config.js"
IMPORT_OR_REQUIRE="require"
fi
# If this file doesn't exist at all, we'll get errors below. # If this file doesn't exist at all, we'll get errors below.
if [ ! -f "next.config.js" ]; then if [ ! -f "${NEXT_CONFIG_JS}" ]; then
touch next.config.js touch ${NEXT_CONFIG_JS}
fi fi
if [ ! -f "next.config.dist" ]; then if [ ! -f "next.config.dist" ]; then
cp next.config.js next.config.dist cp $NEXT_CONFIG_JS next.config.dist
fi fi
which js-beautify >/dev/null which js-beautify >/dev/null
@ -37,17 +50,34 @@ if [ $? -ne 0 ]; then
npm i -g js-beautify npm i -g js-beautify
fi fi
js-beautify next.config.dist > next.config.js # js-beautify formats NEXTJS_CONFIG_FILE (ie next.config.js / next.config.mjs) so we can reliably transformable later
echo "" >> next.config.js js-beautify next.config.dist > ${NEXT_CONFIG_JS}
echo "" >> ${NEXT_CONFIG_JS}
WEBPACK_REQ_LINE=$(grep -n "require([\'\"]webpack[\'\"])" next.config.js | cut -d':' -f1) if [ "${IMPORT_OR_REQUIRE}" == "require" ]; then
WEBPACK_REQ_LINE=$(grep -n "require([\'\"]webpack[\'\"])" ${NEXT_CONFIG_JS} | cut -d':' -f1)
if [ -z "$WEBPACK_REQ_LINE" ]; then if [ -z "$WEBPACK_REQ_LINE" ]; then
cat > next.config.js.0 <<EOF cat > ${NEXT_CONFIG_JS}.0 <<EOF
const webpack = require('webpack'); const webpack = require('webpack');
EOF EOF
fi fi
else
WEBPACK_IMPORT_LINE=$(grep -n "^import .*[\'\"]webpack[\'\"];?$" ${NEXT_CONFIG_JS} | cut -d':' -f1)
if [ -z "$WEBPACK_IMPORT_LINE" ]; then
cat > ${NEXT_CONFIG_JS}.0 <<EOF
import webpack from 'webpack';
EOF
fi
CREATE_REQUIRE_LINE=$(grep -n "require = createRequire" ${NEXT_CONFIG_JS} | cut -d':' -f1)
if [ -z "$CREATE_REQUIRE_LINE" ]; then
cat >> ${NEXT_CONFIG_JS}.0 <<EOF
import { createRequire } from "module";
const require = createRequire(import.meta.url);
EOF
fi
fi
cat > next.config.js.1 <<EOF cat > ${NEXT_CONFIG_JS}.1 <<EOF
let envMap; let envMap;
try { try {
// .env-list.json provides us a list of identifiers which should be replaced at runtime. // .env-list.json provides us a list of identifiers which should be replaced at runtime.
@ -55,7 +85,8 @@ try {
a[v] = \`"CERC_RUNTIME_ENV_\${v.split(/\./).pop()}"\`; a[v] = \`"CERC_RUNTIME_ENV_\${v.split(/\./).pop()}"\`;
return a; return a;
}, {}); }, {});
} catch { } catch (e) {
console.error(e);
// If .env-list.json cannot be loaded, we are probably running in dev mode, so use process.env instead. // If .env-list.json cannot be loaded, we are probably running in dev mode, so use process.env instead.
envMap = Object.keys(process.env).reduce((a, v) => { envMap = Object.keys(process.env).reduce((a, v) => {
if (v.startsWith('CERC_')) { if (v.startsWith('CERC_')) {
@ -64,40 +95,80 @@ try {
return a; return a;
}, {}); }, {});
} }
console.log(envMap);
EOF EOF
CONFIG_LINES=$(wc -l next.config.js | awk '{ print $1 }') grep 'withPWA' ${NEXT_CONFIG_JS} >/dev/null && HAS_WITHPWA=true || HAS_WITHPWA=false
ENV_LINE=$(grep -n 'env:' next.config.js | cut -d':' -f1)
WEBPACK_CONF_LINE=$(egrep -n 'webpack:\s+\([^,]+,' next.config.js | cut -d':' -f1)
NEXT_SECTION_ADJUSTMENT=0
if [ -n "$WEBPACK_CONF_LINE" ]; then if [ "$HAS_WITHPWA" == "true" ]; then
WEBPACK_CONF_VAR=$(egrep -n 'webpack:\s+\([^,]+,' next.config.js | cut -d',' -f1 | cut -d'(' -f2) if [ "$IMPORT_OR_REQUIRE" == "import" ]; then
head -$(( ${WEBPACK_CONF_LINE} )) next.config.js > next.config.js.2 cat > ${NEXT_CONFIG_JS}.2 <<EOF
cat > next.config.js.3 <<EOF const __xPWA__ = (p) => {
$WEBPACK_CONF_VAR.plugins.push(new webpack.DefinePlugin(envMap)); const realPWA = withPWA(p);
EOF return (nextConfig) => {
NEXT_SECTION_LINE=$((WEBPACK_CONF_LINE)) const modConfig = {...nextConfig};
elif [ -n "$ENV_LINE" ]; then
head -$(( ${ENV_LINE} - 1 )) next.config.js > next.config.js.2 modConfig.webpack = (config) => {
cat > next.config.js.3 <<EOF
webpack: (config) => {
config.plugins.push(new webpack.DefinePlugin(envMap)); config.plugins.push(new webpack.DefinePlugin(envMap));
return config; return nextConfig.webpack ? nextConfig.webpack(config) : config;
}, };
return realPWA(modConfig);
};
};
EOF EOF
NEXT_SECTION_ADJUSTMENT=1
NEXT_SECTION_LINE=$ENV_LINE
else else
echo "WARNING: Cannot find location to insert environment variable map in next.config.js" 1>&2 cat > ${NEXT_CONFIG_JS}.3 <<EOF
rm -f next.config.js.* const __xPWA__ = (nextConfig) => {
NEXT_SECTION_LINE=0 const modConfig = {...nextConfig};
modConfig.webpack = (config) => {
config.plugins.push(new webpack.DefinePlugin(envMap));
return nextConfig.webpack ? nextConfig.webpack(config) : config;
};
return withPWA(modConfig);
};
EOF
fi fi
tail -$(( ${CONFIG_LINES} - ${NEXT_SECTION_LINE} + ${NEXT_SECTION_ADJUSTMENT} )) next.config.js > next.config.js.5 cat ${NEXT_CONFIG_JS} | js-beautify | sed 's/withPWA(/__xPWA__(/g' > ${NEXT_CONFIG_JS}.4
else
cat > ${NEXT_CONFIG_JS}.3 <<EOF
const __xCfg__ = (nextConfig) => {
const modConfig = {...nextConfig};
cat next.config.js.* | sed 's/^ *//g' | js-beautify | grep -v 'process\.\env\.' | js-beautify > next.config.js modConfig.webpack = (config) => {
rm next.config.js.* config.plugins.push(new webpack.DefinePlugin(envMap));
return nextConfig.webpack ? nextConfig.webpack(config) : config;
};
return modConfig;
};
EOF
if [ "$IMPORT_OR_REQUIRE" == "import" ]; then
cat ${NEXT_CONFIG_JS} | js-beautify | sed 's/export\s\+default\s\+/const __orig_cfg__ = /g' > ${NEXT_CONFIG_JS}.4
echo "export default __xCfg__(__orig_cfg__);" > ${NEXT_CONFIG_JS}.5
else
cat ${NEXT_CONFIG_JS} | js-beautify | sed 's/module.exports\s\+=\s\+/const __orig_cfg__ = /g' > ${NEXT_CONFIG_JS}.4
echo "module.exports = __xCfg__(__orig_cfg__);" > ${NEXT_CONFIG_JS}.5
fi
fi
rm -f ${NEXT_CONFIG_JS}
for ((i=0; i <= 10; i++)); do
if [ -s "${NEXT_CONFIG_JS}.${i}" ]; then
if [ $i -le 2 ] ; then
cat ${NEXT_CONFIG_JS}.${i} >> ${NEXT_CONFIG_JS}
else
cat ${NEXT_CONFIG_JS}.${i} | sed 's/^ *//g' | js-beautify | grep -v 'process\.\env\.' | js-beautify >> ${NEXT_CONFIG_JS}
fi
fi
done
rm ${NEXT_CONFIG_JS}.*
cat ${NEXT_CONFIG_JS} | js-beautify > ${NEXT_CONFIG_JS}.pretty
mv ${NEXT_CONFIG_JS}.pretty ${NEXT_CONFIG_JS}
"${SCRIPT_DIR}/find-env.sh" "$(pwd)" > .env-list.json "${SCRIPT_DIR}/find-env.sh" "$(pwd)" > .env-list.json
@ -105,8 +176,6 @@ if [ ! -f "package.dist" ]; then
cp package.json package.dist cp package.json package.dist
fi fi
cat package.dist | jq '.scripts.cerc_compile = "next experimental-compile"' | jq '.scripts.cerc_generate = "next experimental-generate"' > package.json
CUR_NEXT_VERSION="`jq -r '.dependencies.next' package.json`" CUR_NEXT_VERSION="`jq -r '.dependencies.next' package.json`"
if [ "$CERC_NEXT_VERSION" != "keep" ] && [ "$CUR_NEXT_VERSION" != "$CERC_NEXT_VERSION" ]; then if [ "$CERC_NEXT_VERSION" != "keep" ] && [ "$CUR_NEXT_VERSION" != "$CERC_NEXT_VERSION" ]; then
@ -115,10 +184,38 @@ if [ "$CERC_NEXT_VERSION" != "keep" ] && [ "$CUR_NEXT_VERSION" != "$CERC_NEXT_VE
mv package.json.$$ package.json mv package.json.$$ package.json
fi fi
CUR_WEBPACK_VERSION="`jq -r '.dependencies.webpack' package.json`"
if [ -z "$CUR_WEBPACK_VERSION" ]; then
CUR_WEBPACK_VERSION="`jq -r '.devDependencies.webpack' package.json`"
fi
if [ "${CERC_WEBPACK_VERSION}" != "keep" ] || [ "${CUR_WEBPACK_VERSION}" == "null" ]; then
if [ -z "$CERC_WEBPACK_VERSION" ] || [ "$CERC_WEBPACK_VERSION" == "keep" ]; then
CERC_WEBPACK_VERSION="${CERC_DEFAULT_WEBPACK_VER}"
fi
echo "Webpack is required for env variable substitution. Adding to webpack@$CERC_WEBPACK_VERSION to dependencies..." 1>&2
cat package.json | jq ".dependencies.webpack = \"$CERC_WEBPACK_VERSION\"" > package.json.$$
mv package.json.$$ package.json
fi
time $CERC_BUILD_TOOL install || exit 1 time $CERC_BUILD_TOOL install || exit 1
CUR_NEXT_VERSION=`jq -r '.version' node_modules/next/package.json` CUR_NEXT_VERSION=`jq -r '.version' node_modules/next/package.json`
# See https://github.com/vercel/next.js/discussions/46544
semver -p -r ">=14.2.0" "$CUR_NEXT_VERSION"
if [ $? -eq 0 ]; then
# For >= 14.2.0
CERC_NEXT_COMPILE_COMMAND="next build --experimental-build-mode compile"
CERC_NEXT_GENERATE_COMMAND="next build --experimental-build-mode generate"
else
# For 13.4.2 to 14.1.x
CERC_NEXT_COMPILE_COMMAND="next experimental-compile"
CERC_NEXT_GENERATE_COMMAND="next experimental-generate"
fi
cat package.json | jq ".scripts.cerc_compile = \"$CERC_NEXT_COMPILE_COMMAND\"" | jq ".scripts.cerc_generate = \"$CERC_NEXT_GENERATE_COMMAND\"" > package.json.$$
mv package.json.$$ package.json
semver -p -r ">=$CERC_MIN_NEXTVER" $CUR_NEXT_VERSION semver -p -r ">=$CERC_MIN_NEXTVER" $CUR_NEXT_VERSION
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
cat <<EOF cat <<EOF

View File

@ -20,9 +20,11 @@ for d in $(find . -maxdepth 1 -type d | grep -v '\./\.' | grep '/' | cut -d'/' -
done done
done done
NEXT_CONF="next.config.js next.config.dist" NEXT_CONF="next.config.mjs next.config.js next.config.dist"
for f in $NEXT_CONF; do for f in $NEXT_CONF; do
if [ -f "$f" ]; then
cat "$f" | tr -s '[:blank:]' '\n' | tr -s '[{},()]' '\n' | egrep -o 'process.env.[A-Za-z0-9_]+' >> $TMPF cat "$f" | tr -s '[:blank:]' '\n' | tr -s '[{},()]' '\n' | egrep -o 'process.env.[A-Za-z0-9_]+' >> $TMPF
fi
done done
cat $TMPF | sort -u | jq --raw-input . | jq --slurp . cat $TMPF | sort -u | jq --raw-input . | jq --slurp .

View File

@ -5,7 +5,7 @@ fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CERC_MAX_GENERATE_TIME=${CERC_MAX_GENERATE_TIME:-60} CERC_MAX_GENERATE_TIME=${CERC_MAX_GENERATE_TIME:-120}
tpid="" tpid=""
ctrl_c() { ctrl_c() {
@ -20,6 +20,8 @@ if [ -z "$CERC_BUILD_TOOL" ]; then
CERC_BUILD_TOOL=pnpm CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else else
CERC_BUILD_TOOL=npm CERC_BUILD_TOOL=npm
fi fi

View File

@ -0,0 +1,10 @@
FROM cerc/ping-pub-base:local
COPY ./scripts/update-explorer-config.sh /scripts
COPY ./scripts/start-serving-explorer.sh /scripts
COPY ./config/laconic-chaindata-template.json /config/chains/laconic-chaindata-template.json
EXPOSE 5173
WORKDIR /app
CMD ["/scripts/start-serving-explorer.sh"]

View File

@ -0,0 +1,8 @@
FROM cerc/webapp-base:local
WORKDIR /app
COPY . .
RUN yarn

View File

@ -1,5 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build the ping pub image # Build the ping pub image
source ${CERC_CONTAINER_BASE_DIR}/build-base.sh source ${CERC_CONTAINER_BASE_DIR}/build-base.sh
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
docker build -t cerc/ping-pub:local ${build_command_args} -f $CERC_REPO_BASE_DIR/explorer/Dockerfile $CERC_REPO_BASE_DIR/explorer # Two-stage build is to allow us to pick up both the upstream repo's files, and local files here for config
docker build -t cerc/ping-pub-base:local ${build_command_args} -f $SCRIPT_DIR/Dockerfile.base $CERC_REPO_BASE_DIR/cosmos-explorer
if [[ $? -ne 0 ]]; then
echo "FATAL: Base container build failed, exiting"
exit 1
fi
docker build -t cerc/ping-pub:local ${build_command_args} -f $SCRIPT_DIR/Dockerfile $SCRIPT_DIR

View File

@ -0,0 +1,22 @@
{
"chain_name": "LACONIC_LACONICD_CHAIN_ID",
"registry_name": "LACONIC_LACONICD_CHAIN_ID",
"api": [
{"provider": "LX-one-tree-one-seven", "address": "LACONIC_LACONICD_API_URL"}
],
"rpc": [
{"provider": "LX-tendermint-rpc", "address": "LACONIC_LACONICD_RPC_URL"}
],
"sdk_version": "0.50.3",
"coin_type": "118",
"min_tx_fee": "800",
"addr_prefix": "laconic",
"logo": "/logos/cosmos.svg",
"assets": [{
"base": "alnt",
"symbol": "LNT",
"exponent": "18",
"coingecko_id": "cosmos",
"logo": "/logos/cosmos.svg"
}]
}

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
${SCRIPT_DIR}/update-explorer-config.sh
echo "Starting serving explorer"
# Force cache re-build because vite is dumb and can't be restarted otherwise
yarn serve --host --force

View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
# Verify that we have the config variables we need
if [[ -z ${LACONIC_LACONICD_API_URL} ]]; then
echo "Error: LACONIC_LACONICD_API_URL not defined"
exit 1
fi
if [[ -z ${LACONIC_LACONICD_RPC_URL} ]]; then
echo "Error: LACONIC_LACONICD_RPC_URL not defined"
exit 1
fi
if [[ -z ${LACONIC_LACONICD_CHAIN_ID} ]]; then
echo "Error: LACONIC_LACONICD_CHAIN_ID not defined"
exit 1
fi
# Ping-pub explorer has endlessly confusing behavior where it
# infers the directory from which to load chain configuration files
# by the presence or absense of the substring "testnet" in the host name
# (browser side -- the host name of the host in the address bar of the browser)
# Accordingly we configure our network in both directories in order to
# subvert this lunacy.
explorer_mainnet_config_dir=/app/chains/mainnet
explorer_testnet_config_dir=/app/chains/testnet
config_template_file=/config/chains/laconic-chaindata-template.json
chain_config_name=laconic.json
mainnet_config_file=${explorer_mainnet_config_dir}/${chain_config_name}
testnet_config_file=${explorer_testnet_config_dir}/${chain_config_name}
# Delete the stock config files
rm -f ${explorer_testnet_config_dir}/*
rm -f ${explorer_mainnet_config_dir}/*
# Copy in our template file
cp ${config_template_file} ${mainnet_config_file}
# Update the file with the config variables
sed -i "s#LACONIC_LACONICD_API_URL#${LACONIC_LACONICD_API_URL}#g" ${mainnet_config_file}
sed -i "s#LACONIC_LACONICD_RPC_URL#${LACONIC_LACONICD_RPC_URL}#g" ${mainnet_config_file}
sed -i "s#LACONIC_LACONICD_CHAIN_ID#${LACONIC_LACONICD_CHAIN_ID}#g" ${mainnet_config_file}
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
echo "Updated chaindata file:"
cat ${mainnet_config_file}
fi
# Copy over to the testnet directory
cp ${mainnet_config_file} ${testnet_config_file}

View File

@ -28,6 +28,8 @@ RUN \
&& su ${USERNAME} -c "umask 0002 && npm install -g semver" \ && su ${USERNAME} -c "umask 0002 && npm install -g semver" \
# Install pnpm # Install pnpm
&& su ${USERNAME} -c "umask 0002 && npm install -g pnpm" \ && su ${USERNAME} -c "umask 0002 && npm install -g pnpm" \
# Install bun
&& su ${USERNAME} -c "umask 0002 && npm install -g bun@1.1.x" \
&& npm cache clean --force > /dev/null 2>&1 && npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages. # [Optional] Uncomment this section to install additional OS packages.

View File

@ -4,6 +4,8 @@ if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x set -x
fi fi
# TODO: document what this script does
WORK_DIR="${1:-./}" WORK_DIR="${1:-./}"
cd "${WORK_DIR}" || exit 1 cd "${WORK_DIR}" || exit 1

View File

@ -27,6 +27,8 @@ elif [ -f "${WORK_DIR}/package.json" ]; then
CERC_BUILD_TOOL=pnpm CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else else
CERC_BUILD_TOOL=npm CERC_BUILD_TOOL=npm
fi fi

View File

@ -1,4 +1,4 @@
laconic-sdk registry-sdk
laconic-registry-cli laconic-registry-cli
laconic-console laconic-console
debug debug

View File

@ -7,7 +7,7 @@ github.com/cerc-io/ipld-eth-db-validator
github.com/cerc-io/ipld-eth-beacon-indexer github.com/cerc-io/ipld-eth-beacon-indexer
github.com/cerc-io/ipld-eth-beacon-db github.com/cerc-io/ipld-eth-beacon-db
github.com/cerc-io/laconicd github.com/cerc-io/laconicd
github.com/cerc-io/laconic-sdk github.com/cerc-io/registry-sdk
github.com/cerc-io/laconic-registry-cli github.com/cerc-io/laconic-registry-cli
github.com/cerc-io/laconic-console github.com/cerc-io/laconic-console
github.com/cerc-io/mobymask-watcher-ts github.com/cerc-io/mobymask-watcher-ts

View File

@ -58,5 +58,5 @@ Now npm packages can be built:
Ensure that `CERC_NPM_AUTH_TOKEN` is set with the token printed above when the package-registry stack was deployed (the actual token value will be different than shown in this example): Ensure that `CERC_NPM_AUTH_TOKEN` is set with the token printed above when the package-registry stack was deployed (the actual token value will be different than shown in this example):
``` ```
$ export CERC_NPM_AUTH_TOKEN=84fe66a73698bf11edbdccd0a338236b7d1d5c45 $ export CERC_NPM_AUTH_TOKEN=84fe66a73698bf11edbdccd0a338236b7d1d5c45
$ laconic-so build-npms --include laconic-sdk,laconic-registry-cli $ laconic-so build-npms --include registry-sdk,laconic-registry-cli
``` ```

View File

@ -49,7 +49,7 @@ $ laconic-so --stack fixturenet-laconic-loaded deploy logs
``` ```
## 6. Test with the Registry CLI ## 6. Test with the Registry CLI
``` ```
$ laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns status" $ laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic registry status"
``` ```
## 7. View the laconic console ## 7. View the laconic console
Get the URL for the console web app with this command (the port number will be different for each deployment): Get the URL for the console web app with this command (the port number will be different for each deployment):
@ -64,5 +64,6 @@ $ laconic-so --stack fixturenet-laconic-loaded deploy exec cli ./scripts/create-
Balance is: 99998999999999998999600000 Balance is: 99998999999999998999600000
Created bond with id: dd88e8d6f9567b32b28e70552aea4419c5dd3307ebae85a284d1fe38904e301a Created bond with id: dd88e8d6f9567b32b28e70552aea4419c5dd3307ebae85a284d1fe38904e301a
Published demo-record-1.yml with id: bafyreierh3xnfivexlscdwubvczmddsnf46uytyfvrbdhkjzztvsz6ruly Published demo-record-1.yml with id: bafyreierh3xnfivexlscdwubvczmddsnf46uytyfvrbdhkjzztvsz6ruly
...
``` ```
The published record should be visible in the console. The published records should be visible in the console.

View File

@ -7,11 +7,11 @@ repos:
- github.com/lirewine/crypto - github.com/lirewine/crypto
- github.com/lirewine/gem - github.com/lirewine/gem
- github.com/lirewine/sdk - github.com/lirewine/sdk
- git.vdb.to/cerc-io/laconic-sdk - git.vdb.to/cerc-io/registry-sdk
- git.vdb.to/cerc-io/laconic-registry-cli - git.vdb.to/cerc-io/laconic-registry-cli
- git.vdb.to/cerc-io/laconic-console - git.vdb.to/cerc-io/laconic-console
npms: npms:
- laconic-sdk - registry-sdk
- laconic-registry-cli - laconic-registry-cli
- debug - debug
- crypto - crypto
@ -30,4 +30,3 @@ config:
cli: cli:
key: laconicd.mykey key: laconicd.mykey
address: laconicd.myaddress address: laconicd.myaddress

View File

@ -44,5 +44,5 @@ $ laconic-so --stack fixturenet-laconicd deploy logs
``` ```
## 6. Test with the Registry CLI ## 6. Test with the Registry CLI
``` ```
$ laconic-so --stack fixturenet-laconicd deploy exec cli "laconic cns status" $ laconic-so --stack fixturenet-laconicd deploy exec cli "laconic registry status"
``` ```

View File

@ -3,10 +3,10 @@ name: fixturenet-laconicd
description: "A laconicd fixturenet" description: "A laconicd fixturenet"
repos: repos:
- git.vdb.to/cerc-io/laconicd - git.vdb.to/cerc-io/laconicd
- git.vdb.to/cerc-io/laconic-sdk - git.vdb.to/cerc-io/registry-sdk
- git.vdb.to/cerc-io/laconic-registry-cli - git.vdb.to/cerc-io/laconic-registry-cli
npms: npms:
- laconic-sdk - registry-sdk
- laconic-registry-cli - laconic-registry-cli
containers: containers:
- cerc/laconicd - cerc/laconicd

View File

@ -18,11 +18,10 @@ from stack_orchestrator.deploy.deploy_types import DeployCommandContext, Laconic
from stack_orchestrator.deploy.deployment_context import DeploymentContext from stack_orchestrator.deploy.deployment_context import DeploymentContext
from stack_orchestrator.deploy.stack_state import State from stack_orchestrator.deploy.stack_state import State
from stack_orchestrator.deploy.deploy_util import VolumeMapping, run_container_command from stack_orchestrator.deploy.deploy_util import VolumeMapping, run_container_command
from stack_orchestrator.command_types import CommandOptions from stack_orchestrator.opts import opts
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from shutil import copyfile, copytree from shutil import copyfile, copytree
import json
import os import os
import sys import sys
import tomli import tomli
@ -34,8 +33,9 @@ default_spec_file_content = ""
class SetupPhase(Enum): class SetupPhase(Enum):
INITIALIZE = 1 INITIALIZE = 1
JOIN = 2 JOIN = 2
CREATE = 3 CONNECT = 3
ILLEGAL = 3 CREATE = 4
ILLEGAL = 5
def _client_toml_path(network_dir: Path): def _client_toml_path(network_dir: Path):
@ -62,39 +62,23 @@ def _get_node_moniker_from_config(network_dir: Path):
return moniker return moniker
def _get_node_key_from_gentx(options: CommandOptions, gentx_file_name: str):
gentx_file_path = Path(gentx_file_name)
if gentx_file_path.exists():
with open(Path(gentx_file_name), "rb") as f:
parsed_json = json.load(f)
return parsed_json['body']['messages'][0]['delegator_address']
else:
print(f"Error: gentx file: {gentx_file_name} does not exist")
sys.exit(1)
def _comma_delimited_to_list(list_str: str): def _comma_delimited_to_list(list_str: str):
return list_str.split(",") if list_str else [] return list_str.split(",") if list_str else []
def _get_node_keys_from_gentx_files(options: CommandOptions, gentx_file_list: str): def _get_node_keys_from_gentx_files(gentx_address_list: str):
node_keys = [] gentx_addresses = _comma_delimited_to_list(gentx_address_list)
gentx_files = _comma_delimited_to_list(gentx_file_list) return gentx_addresses
for gentx_file in gentx_files:
node_key = _get_node_key_from_gentx(options, gentx_file)
if node_key:
node_keys.append(node_key)
return node_keys
def _copy_gentx_files(options: CommandOptions, network_dir: Path, gentx_file_list: str): def _copy_gentx_files(network_dir: Path, gentx_file_list: str):
gentx_files = _comma_delimited_to_list(gentx_file_list) gentx_files = _comma_delimited_to_list(gentx_file_list)
for gentx_file in gentx_files: for gentx_file in gentx_files:
gentx_file_path = Path(gentx_file) gentx_file_path = Path(gentx_file)
copyfile(gentx_file_path, os.path.join(network_dir, "config", "gentx", os.path.basename(gentx_file_path))) copyfile(gentx_file_path, os.path.join(network_dir, "config", "gentx", os.path.basename(gentx_file_path)))
def _remove_persistent_peers(options: CommandOptions, network_dir: Path): def _remove_persistent_peers(network_dir: Path):
config_file_path = _config_toml_path(network_dir) config_file_path = _config_toml_path(network_dir)
if not config_file_path.exists(): if not config_file_path.exists():
print("Error: config.toml not found") print("Error: config.toml not found")
@ -108,20 +92,74 @@ def _remove_persistent_peers(options: CommandOptions, network_dir: Path):
output_file.write(config_file_content) output_file.write(config_file_content)
def _insert_persistent_peers(options: CommandOptions, config_dir: Path, new_persistent_peers: str): def _insert_persistent_peers(config_dir: Path, new_persistent_peers: str):
config_file_path = config_dir.joinpath("config.toml") config_file_path = config_dir.joinpath("config.toml")
if not config_file_path.exists(): if not config_file_path.exists():
print("Error: config.toml not found") print("Error: config.toml not found")
sys.exit(1) sys.exit(1)
with open(config_file_path, "r") as input_file: with open(config_file_path, "r") as input_file:
config_file_content = input_file.read() config_file_content = input_file.read()
persistent_peers_pattern = '^persistent_peers = ""' persistent_peers_pattern = r'^persistent_peers = ""'
replace_with = f"persistent_peers = \"{new_persistent_peers}\"" replace_with = f"persistent_peers = \"{new_persistent_peers}\""
config_file_content = re.sub(persistent_peers_pattern, replace_with, config_file_content, flags=re.MULTILINE) config_file_content = re.sub(persistent_peers_pattern, replace_with, config_file_content, flags=re.MULTILINE)
with open(config_file_path, "w") as output_file: with open(config_file_path, "w") as output_file:
output_file.write(config_file_content) output_file.write(config_file_content)
def _enable_cors(config_dir: Path):
config_file_path = config_dir.joinpath("config.toml")
if not config_file_path.exists():
print("Error: config.toml not found")
sys.exit(1)
with open(config_file_path, "r") as input_file:
config_file_content = input_file.read()
cors_pattern = r'^cors_allowed_origins = \[]'
replace_with = 'cors_allowed_origins = ["*"]'
config_file_content = re.sub(cors_pattern, replace_with, config_file_content, flags=re.MULTILINE)
with open(config_file_path, "w") as output_file:
output_file.write(config_file_content)
app_file_path = config_dir.joinpath("app.toml")
if not app_file_path.exists():
print("Error: app.toml not found")
sys.exit(1)
with open(app_file_path, "r") as input_file:
app_file_content = input_file.read()
cors_pattern = r'^enabled-unsafe-cors = false'
replace_with = "enabled-unsafe-cors = true"
app_file_content = re.sub(cors_pattern, replace_with, app_file_content, flags=re.MULTILINE)
with open(app_file_path, "w") as output_file:
output_file.write(app_file_content)
def _set_listen_address(config_dir: Path):
config_file_path = config_dir.joinpath("config.toml")
if not config_file_path.exists():
print("Error: config.toml not found")
sys.exit(1)
with open(config_file_path, "r") as input_file:
config_file_content = input_file.read()
existing_pattern = r'^laddr = "tcp://127.0.0.1:26657"'
replace_with = 'laddr = "tcp://0.0.0.0:26657"'
print(f"Replacing in: {config_file_path}")
config_file_content = re.sub(existing_pattern, replace_with, config_file_content, flags=re.MULTILINE)
with open(config_file_path, "w") as output_file:
output_file.write(config_file_content)
app_file_path = config_dir.joinpath("app.toml")
if not app_file_path.exists():
print("Error: app.toml not found")
sys.exit(1)
with open(app_file_path, "r") as input_file:
app_file_content = input_file.read()
existing_pattern1 = r'^address = "tcp://localhost:1317"'
replace_with1 = 'address = "tcp://0.0.0.0:1317"'
app_file_content = re.sub(existing_pattern1, replace_with1, app_file_content, flags=re.MULTILINE)
existing_pattern2 = r'^address = "localhost:9090"'
replace_with2 = 'address = "0.0.0.0:9090"'
app_file_content = re.sub(existing_pattern2, replace_with2, app_file_content, flags=re.MULTILINE)
with open(app_file_path, "w") as output_file:
output_file.write(app_file_content)
def _phase_from_params(parameters): def _phase_from_params(parameters):
phase = SetupPhase.ILLEGAL phase = SetupPhase.ILLEGAL
if parameters.initialize_network: if parameters.initialize_network:
@ -146,14 +184,19 @@ def _phase_from_params(parameters):
print("Can't supply --initialize-network or --join-network with --create-network") print("Can't supply --initialize-network or --join-network with --create-network")
sys.exit(1) sys.exit(1)
phase = SetupPhase.CREATE phase = SetupPhase.CREATE
elif parameters.connect_network:
if parameters.initialize_network or parameters.join_network:
print("Can't supply --initialize-network or --join-network with --connect-network")
sys.exit(1)
phase = SetupPhase.CONNECT
return phase return phase
def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCommand, extra_args): def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCommand, extra_args):
options = command_context.cluster_context.options options = opts.o
currency = "stake" # Does this need to be a parameter? currency = "alnt" # Does this need to be a parameter?
if options.debug: if options.debug:
print(f"parameters: {parameters}") print(f"parameters: {parameters}")
@ -178,11 +221,12 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
output, status = run_container_command( output, status = run_container_command(
command_context, command_context,
"laconicd", f"laconicd init {parameters.node_moniker} --home {laconicd_home_path_in_container}\ "laconicd", f"laconicd init {parameters.node_moniker} --home {laconicd_home_path_in_container}\
--chain-id {parameters.chain_id}", mounts) --chain-id {parameters.chain_id} --default-denom {currency}", mounts)
if options.debug: if options.debug:
print(f"Command output: {output}") print(f"Command output: {output}")
elif phase == SetupPhase.JOIN: elif phase == SetupPhase.JOIN:
# In the join phase (alternative to connect) we are participating in a genesis ceremony for the chain
if not os.path.exists(network_dir): if not os.path.exists(network_dir):
print(f"Error: network directory {network_dir} doesn't exist") print(f"Error: network directory {network_dir} doesn't exist")
sys.exit(1) sys.exit(1)
@ -197,7 +241,7 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
output2, status2 = run_container_command( output2, status2 = run_container_command(
command_context, command_context,
"laconicd", "laconicd",
f"laconicd add-genesis-account {parameters.key_name} 12900000000000000000000{currency}\ f"laconicd genesis add-genesis-account {parameters.key_name} 12900000000000000000000{currency}\
--home {laconicd_home_path_in_container} --keyring-backend test", --home {laconicd_home_path_in_container} --keyring-backend test",
mounts) mounts)
if options.debug: if options.debug:
@ -205,7 +249,7 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
output3, status3 = run_container_command( output3, status3 = run_container_command(
command_context, command_context,
"laconicd", "laconicd",
f"laconicd gentx {parameters.key_name} 90000000000{currency} --home {laconicd_home_path_in_container}\ f"laconicd genesis gentx {parameters.key_name} 90000000000{currency} --home {laconicd_home_path_in_container}\
--chain-id {chain_id} --keyring-backend test", --chain-id {chain_id} --keyring-backend test",
mounts) mounts)
if options.debug: if options.debug:
@ -215,7 +259,28 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
"laconicd", "laconicd",
f"laconicd keys show {parameters.key_name} -a --home {laconicd_home_path_in_container} --keyring-backend test", f"laconicd keys show {parameters.key_name} -a --home {laconicd_home_path_in_container} --keyring-backend test",
mounts) mounts)
print(f"Node validator address: {output4}") print(f"Node account address: {output4}")
elif phase == SetupPhase.CONNECT:
# In the connect phase (named to not conflict with join) we are making a node that syncs a chain with existing genesis.json
# but not with validator role. We need this kind of node in order to bootstrap it into a validator after it syncs
output1, status1 = run_container_command(
command_context, "laconicd", f"laconicd keys add {parameters.key_name} --home {laconicd_home_path_in_container}\
--keyring-backend test", mounts)
if options.debug:
print(f"Command output: {output1}")
output2, status2 = run_container_command(
command_context,
"laconicd",
f"laconicd keys show {parameters.key_name} -a --home {laconicd_home_path_in_container} --keyring-backend test",
mounts)
print(f"Node account address: {output2}")
output3, status3 = run_container_command(
command_context,
"laconicd",
f"laconicd cometbft show-validator --home {laconicd_home_path_in_container}",
mounts)
print(f"Node validator address: {output3}")
elif phase == SetupPhase.CREATE: elif phase == SetupPhase.CREATE:
if not os.path.exists(network_dir): if not os.path.exists(network_dir):
@ -234,32 +299,30 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
copyfile(genesis_file_path, os.path.join(network_dir, "config", os.path.basename(genesis_file_path))) copyfile(genesis_file_path, os.path.join(network_dir, "config", os.path.basename(genesis_file_path)))
else: else:
# We're generating the genesis file # We're generating the genesis file
if not parameters.gentx_file_list:
print("Error: --gentx-files must be supplied")
sys.exit(1)
# First look in the supplied gentx files for the other nodes' keys # First look in the supplied gentx files for the other nodes' keys
other_node_keys = _get_node_keys_from_gentx_files(options, parameters.gentx_file_list) other_node_keys = _get_node_keys_from_gentx_files(parameters.gentx_address_list)
# Add those keys to our genesis, with balances we determine here (why?) # Add those keys to our genesis, with balances we determine here (why?)
for other_node_key in other_node_keys: for other_node_key in other_node_keys:
outputk, statusk = run_container_command( outputk, statusk = run_container_command(
command_context, "laconicd", f"laconicd add-genesis-account {other_node_key} 12900000000000000000000{currency}\ command_context, "laconicd", f"laconicd genesis add-genesis-account {other_node_key} \
12900000000000000000000{currency}\
--home {laconicd_home_path_in_container} --keyring-backend test", mounts) --home {laconicd_home_path_in_container} --keyring-backend test", mounts)
if options.debug: if options.debug:
print(f"Command output: {outputk}") print(f"Command output: {outputk}")
# Copy the gentx json files into our network dir # Copy the gentx json files into our network dir
_copy_gentx_files(options, network_dir, parameters.gentx_file_list) _copy_gentx_files(network_dir, parameters.gentx_file_list)
# Now we can run collect-gentxs # Now we can run collect-gentxs
output1, status1 = run_container_command( output1, status1 = run_container_command(
command_context, "laconicd", f"laconicd collect-gentxs --home {laconicd_home_path_in_container}", mounts) command_context, "laconicd", f"laconicd genesis collect-gentxs --home {laconicd_home_path_in_container}", mounts)
if options.debug: if options.debug:
print(f"Command output: {output1}") print(f"Command output: {output1}")
print(f"Generated genesis file, please copy to other nodes as required: \ print(f"Generated genesis file, please copy to other nodes as required: \
{os.path.join(network_dir, 'config', 'genesis.json')}") {os.path.join(network_dir, 'config', 'genesis.json')}")
# Last thing, collect-gentxs puts a likely bogus set of persistent_peers in config.toml so we remove that now # Last thing, collect-gentxs puts a likely bogus set of persistent_peers in config.toml so we remove that now
_remove_persistent_peers(options, network_dir) _remove_persistent_peers(network_dir)
# In both cases we validate the genesis file now # In both cases we validate the genesis file now
output2, status1 = run_container_command( output2, status1 = run_container_command(
command_context, "laconicd", f"laconicd validate-genesis --home {laconicd_home_path_in_container}", mounts) command_context, "laconicd", f"laconicd genesis validate-genesis --home {laconicd_home_path_in_container}", mounts)
print(f"validate-genesis result: {output2}") print(f"validate-genesis result: {output2}")
else: else:
@ -267,7 +330,7 @@ def setup(command_context: DeployCommandContext, parameters: LaconicStackSetupCo
sys.exit(1) sys.exit(1)
def create(context: DeploymentContext, extra_args): def create(deployment_context: DeploymentContext, extra_args):
network_dir = extra_args[0] network_dir = extra_args[0]
if network_dir is None: if network_dir is None:
print("Error: --network-dir must be supplied") print("Error: --network-dir must be supplied")
@ -286,15 +349,18 @@ def create(context: DeploymentContext, extra_args):
sys.exit(1) sys.exit(1)
# Copy the network directory contents into our deployment # Copy the network directory contents into our deployment
# TODO: change this to work with non local paths # TODO: change this to work with non local paths
deployment_config_dir = context.deployment_dir.joinpath("data", "laconicd-config") deployment_config_dir = deployment_context.deployment_dir.joinpath("data", "laconicd-config")
copytree(config_dir_path, deployment_config_dir, dirs_exist_ok=True) copytree(config_dir_path, deployment_config_dir, dirs_exist_ok=True)
# If supplied, add the initial persistent peers to the config file # If supplied, add the initial persistent peers to the config file
if extra_args[1]: if extra_args[1]:
initial_persistent_peers = extra_args[1] initial_persistent_peers = extra_args[1]
_insert_persistent_peers(context.command_context.cluster_context.options, deployment_config_dir, initial_persistent_peers) _insert_persistent_peers(deployment_config_dir, initial_persistent_peers)
# Enable CORS headers so explorers and so on can talk to the node
_enable_cors(deployment_config_dir)
_set_listen_address(deployment_config_dir)
# Copy the data directory contents into our deployment # Copy the data directory contents into our deployment
# TODO: change this to work with non local paths # TODO: change this to work with non local paths
deployment_data_dir = context.deployment_dir.joinpath("data", "laconicd-data") deployment_data_dir = deployment_context.deployment_dir.joinpath("data", "laconicd-data")
copytree(data_dir_path, deployment_data_dir, dirs_exist_ok=True) copytree(data_dir_path, deployment_data_dir, dirs_exist_ok=True)
@ -304,7 +370,6 @@ def init(command_context: DeployCommandContext):
def get_state(command_context: DeployCommandContext): def get_state(command_context: DeployCommandContext):
print("Here we get state")
return State.CONFIGURED return State.CONFIGURED

View File

@ -2,16 +2,17 @@ version: "1.0"
name: mainnet-laconic name: mainnet-laconic
description: "Mainnet laconic node" description: "Mainnet laconic node"
repos: repos:
- cerc-io/laconicd - git.vdb.to/cerc-io/laconicd
- lirewine/debug - github.com/lirewine/debug
- lirewine/crypto - github.com/lirewine/crypto
- lirewine/gem - github.com/lirewine/gem
- lirewine/sdk - github.com/lirewine/sdk
- cerc-io/laconic-sdk - git.vdb.to/cerc-io/registry-sdk
- cerc-io/laconic-registry-cli - git.vdb.to/cerc-io/laconic-registry-cli
- cerc-io/laconic-console - git.vdb.to/cerc-io/laconic-console
- git.vdb.to/cerc-io/cosmos-explorer
npms: npms:
- laconic-sdk - registry-sdk
- laconic-registry-cli - laconic-registry-cli
- debug - debug
- crypto - crypto
@ -23,7 +24,8 @@ containers:
- cerc/laconic-registry-cli - cerc/laconic-registry-cli
- cerc/webapp-base - cerc/webapp-base
- cerc/laconic-console-host - cerc/laconic-console-host
- cerc/ping-pub
pods: pods:
- mainnet-laconicd - mainnet-laconicd
- fixturenet-laconic-console - fixturenet-laconic-console
- laconic-explorer

View File

@ -8,8 +8,11 @@ echo "Environment variables:"
env env
# Test laconic stack # Test laconic stack
echo "Running laconic stack test" echo "Running laconic stack test"
# Bit of a hack, test the most recent package if [ "$1" == "from-path" ]; then
TEST_TARGET_SO="laconic-so"
else
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 ) TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
fi
# Set a non-default repo dir # Set a non-default repo dir
export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir
echo "Testing this package: $TEST_TARGET_SO" echo "Testing this package: $TEST_TARGET_SO"

View File

@ -109,7 +109,7 @@ Setup a test chain:
```bash ```bash
export CERC_NPM_REGISTRY_URL=https://git.vdb.to/api/packages/cerc-io/npm/ export CERC_NPM_REGISTRY_URL=https://git.vdb.to/api/packages/cerc-io/npm/
laconic-so --stack fixturenet-laconic-loaded setup-repositories --include git.vdb.to/cerc-io/laconicd,git.vdb.to/cerc-io/laconic-sdk,git.vdb.to/cerc-io/laconic-registry-cli,git.vdb.to/cerc-io/laconic-console laconic-so --stack fixturenet-laconic-loaded setup-repositories --include git.vdb.to/cerc-io/laconicd,git.vdb.to/cerc-io/registry-sdk,git.vdb.to/cerc-io/laconic-registry-cli,git.vdb.to/cerc-io/laconic-console
laconic-so --stack fixturenet-laconic-loaded build-containers laconic-so --stack fixturenet-laconic-loaded build-containers

View File

@ -2,4 +2,28 @@
The Package Registry Stack supports a build environment that requires a package registry (initially for NPM packages only). The Package Registry Stack supports a build environment that requires a package registry (initially for NPM packages only).
Setup instructions can be found [here](../build-support/README.md). ## Setup
* Setup required repos and build containers:
```bash
laconic-so --stack package-registry setup-repositories
laconic-so --stack package-registry build-containers
```
* Create a deployment:
```bash
laconic-so --stack package-registry deploy init --output package-registry-spec.yml
# Update port mapping in the laconic-loaded.spec file to resolve port conflicts on host if any
laconic-so --stack package-registry deploy create --deployment-dir package-registry-deployment --spec-file package-registry-spec.yml
```
* Start the deployment:
```bash
laconic-so deployment --dir package-registry-deployment start
```
* The local gitea registry can now be accessed at <http://localhost:3000> (the username and password can be taken from the deployment logs)

View File

@ -29,14 +29,14 @@ class DockerDeployer(Deployer):
compose_env_file=compose_env_file) compose_env_file=compose_env_file)
self.type = type self.type = type
def up(self, detach, services): def up(self, detach, skip_cluster_management, services):
if not opts.o.dry_run: if not opts.o.dry_run:
try: try:
return self.docker.compose.up(detach=detach, services=services) return self.docker.compose.up(detach=detach, services=services)
except DockerException as e: except DockerException as e:
raise DeployerException(e) raise DeployerException(e)
def down(self, timeout, volumes): def down(self, timeout, volumes, skip_cluster_management):
if not opts.o.dry_run: if not opts.o.dry_run:
try: try:
return self.docker.compose.down(timeout=timeout, volumes=volumes) return self.docker.compose.down(timeout=timeout, volumes=volumes)

View File

@ -26,8 +26,15 @@ import click
from pathlib import Path from pathlib import Path
from stack_orchestrator import constants from stack_orchestrator import constants
from stack_orchestrator.opts import opts from stack_orchestrator.opts import opts
from stack_orchestrator.util import include_exclude_check, get_parsed_stack_config, global_options2, get_dev_root_path from stack_orchestrator.util import (
from stack_orchestrator.util import resolve_compose_file get_stack_path,
include_exclude_check,
get_parsed_stack_config,
global_options2,
get_dev_root_path,
stack_is_in_deployment,
resolve_compose_file,
)
from stack_orchestrator.deploy.deployer import Deployer, DeployerException from stack_orchestrator.deploy.deployer import Deployer, DeployerException
from stack_orchestrator.deploy.deployer_factory import getDeployer from stack_orchestrator.deploy.deployer_factory import getDeployer
from stack_orchestrator.deploy.deploy_types import ClusterContext, DeployCommandContext from stack_orchestrator.deploy.deploy_types import ClusterContext, DeployCommandContext
@ -60,6 +67,7 @@ def command(ctx, include, exclude, env_file, cluster, deploy_to):
if deploy_to is None: if deploy_to is None:
deploy_to = "compose" deploy_to = "compose"
stack = get_stack_path(stack)
ctx.obj = create_deploy_context(global_options2(ctx), None, stack, include, exclude, cluster, env_file, deploy_to) ctx.obj = create_deploy_context(global_options2(ctx), None, stack, include, exclude, cluster, env_file, deploy_to)
# Subcommand is executed now, by the magic of click # Subcommand is executed now, by the magic of click
@ -83,7 +91,7 @@ def create_deploy_context(
return DeployCommandContext(stack, cluster_context, deployer) return DeployCommandContext(stack, cluster_context, deployer)
def up_operation(ctx, services_list, stay_attached=False): def up_operation(ctx, services_list, stay_attached=False, skip_cluster_management=False):
global_context = ctx.parent.parent.obj global_context = ctx.parent.parent.obj
deploy_context = ctx.obj deploy_context = ctx.obj
cluster_context = deploy_context.cluster_context cluster_context = deploy_context.cluster_context
@ -94,18 +102,18 @@ def up_operation(ctx, services_list, stay_attached=False):
print(f"Running compose up with container_exec_env: {container_exec_env}, extra_args: {services_list}") print(f"Running compose up with container_exec_env: {container_exec_env}, extra_args: {services_list}")
for pre_start_command in cluster_context.pre_start_commands: for pre_start_command in cluster_context.pre_start_commands:
_run_command(global_context, cluster_context.cluster, pre_start_command) _run_command(global_context, cluster_context.cluster, pre_start_command)
deploy_context.deployer.up(detach=not stay_attached, services=services_list) deploy_context.deployer.up(detach=not stay_attached, skip_cluster_management=skip_cluster_management, services=services_list)
for post_start_command in cluster_context.post_start_commands: for post_start_command in cluster_context.post_start_commands:
_run_command(global_context, cluster_context.cluster, post_start_command) _run_command(global_context, cluster_context.cluster, post_start_command)
_orchestrate_cluster_config(global_context, cluster_context.config, deploy_context.deployer, container_exec_env) _orchestrate_cluster_config(global_context, cluster_context.config, deploy_context.deployer, container_exec_env)
def down_operation(ctx, delete_volumes, extra_args_list): def down_operation(ctx, delete_volumes, extra_args_list, skip_cluster_management=False):
timeout_arg = None timeout_arg = None
if extra_args_list: if extra_args_list:
timeout_arg = extra_args_list[0] timeout_arg = extra_args_list[0]
# Specify shutdown timeout (default 10s) to give services enough time to shutdown gracefully # Specify shutdown timeout (default 10s) to give services enough time to shutdown gracefully
ctx.obj.deployer.down(timeout=timeout_arg, volumes=delete_volumes) ctx.obj.deployer.down(timeout=timeout_arg, volumes=delete_volumes, skip_cluster_management=skip_cluster_management)
def status_operation(ctx): def status_operation(ctx):
@ -274,16 +282,12 @@ def _make_default_cluster_name(deployment, compose_dir, stack, include, exclude)
# stack has to be either PathLike pointing to a stack yml file, or a string with the name of a known stack # stack has to be either PathLike pointing to a stack yml file, or a string with the name of a known stack
def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file): def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file):
dev_root_path = get_dev_root_path(ctx) dev_root_path = get_dev_root_path(ctx)
# TODO: huge hack, fix this # TODO: hack, this should be encapsulated by the deployment context.
# If the caller passed a path for the stack file, then we know that we can get the compose files deployment = stack_is_in_deployment(stack)
# from the same directory if deployment:
deployment = False compose_dir = stack.joinpath("compose")
if isinstance(stack, os.PathLike):
compose_dir = stack.parent.joinpath("compose")
deployment = True
else: else:
# See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure # See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure
compose_dir = Path(__file__).absolute().parent.parent.joinpath("data", "compose") compose_dir = Path(__file__).absolute().parent.parent.joinpath("data", "compose")

View File

@ -50,8 +50,10 @@ class LaconicStackSetupCommand:
key_name: str key_name: str
initialize_network: bool initialize_network: bool
join_network: bool join_network: bool
connect_network: bool
create_network: bool create_network: bool
gentx_file_list: str gentx_file_list: str
gentx_address_list: str
genesis_file: str genesis_file: str
network_dir: str network_dir: str

View File

@ -13,7 +13,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>. # along with this program. If not, see <http:#www.gnu.org/licenses/>.
import os
from typing import List, Any from typing import List, Any
from stack_orchestrator.deploy.deploy_types import DeployCommandContext, VolumeMapping from stack_orchestrator.deploy.deploy_types import DeployCommandContext, VolumeMapping
from stack_orchestrator.util import get_parsed_stack_config, get_yaml, get_pod_list, resolve_compose_file from stack_orchestrator.util import get_parsed_stack_config, get_yaml, get_pod_list, resolve_compose_file
@ -83,7 +82,9 @@ def run_container_command(ctx: DeployCommandContext, service: str, command: str,
docker_output = deployer.run( docker_output = deployer.run(
container_image, container_image,
["-c", command], entrypoint="sh", ["-c", command], entrypoint="sh",
user=f"{os.getuid()}:{os.getgid()}", # Current laconicd container has a bug where it crashes when run not as root
# Commented out line below is a workaround. Created files end up owned by root on the host
# user=f"{os.getuid()}:{os.getgid()}",
volumes=docker_volumes volumes=docker_volumes
) )
# There doesn't seem to be a way to get an exit code from docker.run() # There doesn't seem to be a way to get an exit code from docker.run()

View File

@ -20,11 +20,11 @@ from pathlib import Path
class Deployer(ABC): class Deployer(ABC):
@abstractmethod @abstractmethod
def up(self, detach, services): def up(self, detach, skip_cluster_management, services):
pass pass
@abstractmethod @abstractmethod
def down(self, timeout, volumes): def down(self, timeout, volumes, skip_cluster_management):
pass pass
@abstractmethod @abstractmethod

View File

@ -50,58 +50,68 @@ def command(ctx, dir):
def make_deploy_context(ctx) -> DeployCommandContext: def make_deploy_context(ctx) -> DeployCommandContext:
context: DeploymentContext = ctx.obj context: DeploymentContext = ctx.obj
stack_file_path = context.get_stack_file()
env_file = context.get_env_file() env_file = context.get_env_file()
cluster_name = context.get_cluster_id() cluster_name = context.get_cluster_id()
if constants.deploy_to_key in context.spec.obj: if constants.deploy_to_key in context.spec.obj:
deployment_type = context.spec.obj[constants.deploy_to_key] deployment_type = context.spec.obj[constants.deploy_to_key]
else: else:
deployment_type = constants.compose_deploy_type deployment_type = constants.compose_deploy_type
return create_deploy_context(ctx.parent.parent.obj, context, stack_file_path, None, None, cluster_name, env_file, stack = context.deployment_dir
deployment_type) return create_deploy_context(ctx.parent.parent.obj, context, stack, None, None,
cluster_name, env_file, deployment_type)
# TODO: remove legacy up command since it's an alias for start
@command.command() @command.command()
@click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout") @click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout")
@click.option("--skip-cluster-management/--perform-cluster-management",
default=False, help="Skip cluster initialization/tear-down (only for kind-k8s deployments)")
@click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2> @click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2>
@click.pass_context @click.pass_context
def up(ctx, stay_attached, extra_args): def up(ctx, stay_attached, skip_cluster_management, extra_args):
ctx.obj = make_deploy_context(ctx) ctx.obj = make_deploy_context(ctx)
services_list = list(extra_args) or None services_list = list(extra_args) or None
up_operation(ctx, services_list, stay_attached) up_operation(ctx, services_list, stay_attached, skip_cluster_management)
# start is the preferred alias for up # start is the preferred alias for up
@command.command() @command.command()
@click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout") @click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout")
@click.option("--skip-cluster-management/--perform-cluster-management",
default=False, help="Skip cluster initialization/tear-down (only for kind-k8s deployments)")
@click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2> @click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2>
@click.pass_context @click.pass_context
def start(ctx, stay_attached, extra_args): def start(ctx, stay_attached, skip_cluster_management, extra_args):
ctx.obj = make_deploy_context(ctx) ctx.obj = make_deploy_context(ctx)
services_list = list(extra_args) or None services_list = list(extra_args) or None
up_operation(ctx, services_list, stay_attached) up_operation(ctx, services_list, stay_attached, skip_cluster_management)
# TODO: remove legacy up command since it's an alias for stop
@command.command() @command.command()
@click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes") @click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes")
@click.option("--skip-cluster-management/--perform-cluster-management",
default=False, help="Skip cluster initialization/tear-down (only for kind-k8s deployments)")
@click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2> @click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2>
@click.pass_context @click.pass_context
def down(ctx, delete_volumes, extra_args): def down(ctx, delete_volumes, skip_cluster_management, extra_args):
# Get the stack config file name # Get the stack config file name
# TODO: add cluster name and env file here # TODO: add cluster name and env file here
ctx.obj = make_deploy_context(ctx) ctx.obj = make_deploy_context(ctx)
down_operation(ctx, delete_volumes, extra_args) down_operation(ctx, delete_volumes, extra_args, skip_cluster_management)
# stop is the preferred alias for down # stop is the preferred alias for down
@command.command() @command.command()
@click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes") @click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes")
@click.option("--skip-cluster-management/--perform-cluster-management",
default=False, help="Skip cluster initialization/tear-down (only for kind-k8s deployments)")
@click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2> @click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2>
@click.pass_context @click.pass_context
def stop(ctx, delete_volumes, extra_args): def stop(ctx, delete_volumes, skip_cluster_management, extra_args):
# TODO: add cluster name and env file here # TODO: add cluster name and env file here
ctx.obj = make_deploy_context(ctx) ctx.obj = make_deploy_context(ctx)
down_operation(ctx, delete_volumes, extra_args) down_operation(ctx, delete_volumes, extra_args, skip_cluster_management)
@command.command() @command.command()
@ -123,6 +133,7 @@ def push_images(ctx):
@click.argument('extra_args', nargs=-1) # help: command: port <service1> <service2> @click.argument('extra_args', nargs=-1) # help: command: port <service1> <service2>
@click.pass_context @click.pass_context
def port(ctx, extra_args): def port(ctx, extra_args):
ctx.obj = make_deploy_context(ctx)
port_operation(ctx, extra_args) port_operation(ctx, extra_args)

View File

@ -24,7 +24,7 @@ from secrets import token_hex
import sys import sys
from stack_orchestrator import constants from stack_orchestrator import constants
from stack_orchestrator.opts import opts from stack_orchestrator.opts import opts
from stack_orchestrator.util import (get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, from stack_orchestrator.util import (get_stack_path, get_parsed_deployment_spec, get_parsed_stack_config,
global_options, get_yaml, get_pod_list, get_pod_file_path, pod_has_scripts, global_options, get_yaml, get_pod_list, get_pod_file_path, pod_has_scripts,
get_pod_script_paths, get_plugin_code_paths, error_exit, env_var_map_from_file, get_pod_script_paths, get_plugin_code_paths, error_exit, env_var_map_from_file,
resolve_config_dir) resolve_config_dir)
@ -238,6 +238,11 @@ def _find_extra_config_dirs(parsed_pod_file, pod):
config_dir = host_path.split("/")[2] config_dir = host_path.split("/")[2]
if config_dir != pod: if config_dir != pod:
config_dirs.add(config_dir) config_dirs.add(config_dir)
for env_file in service_info.get("env_file", []):
if env_file.startswith("../config"):
config_dir = env_file.split("/")[2]
if config_dir != pod:
config_dirs.add(config_dir)
return config_dirs return config_dirs
@ -454,7 +459,7 @@ def create_operation(deployment_command_context, spec_file, deployment_dir, netw
_check_volume_definitions(parsed_spec) _check_volume_definitions(parsed_spec)
stack_name = parsed_spec["stack"] stack_name = parsed_spec["stack"]
deployment_type = parsed_spec[constants.deploy_to_key] deployment_type = parsed_spec[constants.deploy_to_key]
stack_file = get_stack_file_path(stack_name) stack_file = get_stack_path(stack_name).joinpath(constants.stack_file_name)
parsed_stack = get_parsed_stack_config(stack_name) parsed_stack = get_parsed_stack_config(stack_name)
if opts.o.debug: if opts.o.debug:
print(f"parsed spec: {parsed_spec}") print(f"parsed spec: {parsed_spec}")
@ -467,7 +472,7 @@ def create_operation(deployment_command_context, spec_file, deployment_dir, netw
os.mkdir(deployment_dir_path) os.mkdir(deployment_dir_path)
# Copy spec file and the stack file into the deployment dir # Copy spec file and the stack file into the deployment dir
copyfile(spec_file, deployment_dir_path.joinpath(constants.spec_file_name)) copyfile(spec_file, deployment_dir_path.joinpath(constants.spec_file_name))
copyfile(stack_file, deployment_dir_path.joinpath(os.path.basename(stack_file))) copyfile(stack_file, deployment_dir_path.joinpath(constants.stack_file_name))
_create_deployment_file(deployment_dir_path) _create_deployment_file(deployment_dir_path)
# Copy any config varibles from the spec file into an env file suitable for compose # Copy any config varibles from the spec file into an env file suitable for compose
_write_config_file(spec_file, deployment_dir_path.joinpath(constants.config_file_name)) _write_config_file(spec_file, deployment_dir_path.joinpath(constants.config_file_name))
@ -509,6 +514,23 @@ def create_operation(deployment_command_context, spec_file, deployment_dir, netw
os.mkdir(destination_script_dir) os.mkdir(destination_script_dir)
script_paths = get_pod_script_paths(parsed_stack, pod) script_paths = get_pod_script_paths(parsed_stack, pod)
_copy_files_to_directory(script_paths, destination_script_dir) _copy_files_to_directory(script_paths, destination_script_dir)
if parsed_spec.is_kubernetes_deployment():
for configmap in parsed_spec.get_configmaps():
source_config_dir = resolve_config_dir(stack_name, configmap)
if os.path.exists(source_config_dir):
destination_config_dir = deployment_dir_path.joinpath("configmaps", configmap)
copytree(source_config_dir, destination_config_dir, dirs_exist_ok=True)
else:
# TODO: We should probably only do this if the volume is marked :ro.
for volume_name, volume_path in parsed_spec.get_volumes().items():
source_config_dir = resolve_config_dir(stack_name, volume_name)
# Only copy if the source exists and is _not_ empty.
if os.path.exists(source_config_dir) and os.listdir(source_config_dir):
destination_config_dir = deployment_dir_path.joinpath(volume_path)
# Only copy if the destination exists and _is_ empty.
if os.path.exists(destination_config_dir) and not os.listdir(destination_config_dir):
copytree(source_config_dir, destination_config_dir, dirs_exist_ok=True)
# Delegate to the stack's Python code # Delegate to the stack's Python code
# The deploy create command doesn't require a --stack argument so we need to insert the # The deploy create command doesn't require a --stack argument so we need to insert the
# stack member here. # stack member here.
@ -530,15 +552,17 @@ def create_operation(deployment_command_context, spec_file, deployment_dir, netw
@click.option("--chain-id", help="The new chain id") @click.option("--chain-id", help="The new chain id")
@click.option("--key-name", help="Name for new node key") @click.option("--key-name", help="Name for new node key")
@click.option("--gentx-files", help="List of comma-delimited gentx filenames from other nodes") @click.option("--gentx-files", help="List of comma-delimited gentx filenames from other nodes")
@click.option("--gentx-addresses", type=str, help="List of comma-delimited validator addresses for other nodes")
@click.option("--genesis-file", help="Genesis file for the network") @click.option("--genesis-file", help="Genesis file for the network")
@click.option("--initialize-network", is_flag=True, default=False, help="Initialize phase") @click.option("--initialize-network", is_flag=True, default=False, help="Initialize phase")
@click.option("--join-network", is_flag=True, default=False, help="Join phase") @click.option("--join-network", is_flag=True, default=False, help="Join phase")
@click.option("--connect-network", is_flag=True, default=False, help="Connect phase")
@click.option("--create-network", is_flag=True, default=False, help="Create phase") @click.option("--create-network", is_flag=True, default=False, help="Create phase")
@click.option("--network-dir", help="Directory for network files") @click.option("--network-dir", help="Directory for network files")
@click.argument('extra_args', nargs=-1) @click.argument('extra_args', nargs=-1)
@click.pass_context @click.pass_context
def setup(ctx, node_moniker, chain_id, key_name, gentx_files, genesis_file, initialize_network, join_network, create_network, def setup(ctx, node_moniker, chain_id, key_name, gentx_files, gentx_addresses, genesis_file, initialize_network, join_network,
network_dir, extra_args): connect_network, create_network, network_dir, extra_args):
parmeters = LaconicStackSetupCommand(chain_id, node_moniker, key_name, initialize_network, join_network, create_network, parmeters = LaconicStackSetupCommand(chain_id, node_moniker, key_name, initialize_network, join_network, connect_network,
gentx_files, genesis_file, network_dir) create_network, gentx_files, gentx_addresses, genesis_file, network_dir)
call_stack_deploy_setup(ctx.obj, parmeters, extra_args) call_stack_deploy_setup(ctx.obj, parmeters, extra_args)

View File

@ -14,6 +14,7 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>. # along with this program. If not, see <http:#www.gnu.org/licenses/>.
import os import os
import base64
from kubernetes import client from kubernetes import client
from typing import Any, List, Set from typing import Any, List, Set
@ -78,28 +79,40 @@ class ClusterInfo:
if (opts.o.debug): if (opts.o.debug):
print(f"Env vars: {self.environment_variables.map}") print(f"Env vars: {self.environment_variables.map}")
def get_nodeport(self): def get_nodeports(self):
nodeports = []
for pod_name in self.parsed_pod_yaml_map: for pod_name in self.parsed_pod_yaml_map:
pod = self.parsed_pod_yaml_map[pod_name] pod = self.parsed_pod_yaml_map[pod_name]
services = pod["services"] services = pod["services"]
for service_name in services: for service_name in services:
service_info = services[service_name] service_info = services[service_name]
if "ports" in service_info: if "ports" in service_info:
port = int(service_info["ports"][0]) for raw_port in [str(p) for p in service_info["ports"]]:
if opts.o.debug: if opts.o.debug:
print(f"service port: {port}") print(f"service port: {raw_port}")
if ":" in raw_port:
parts = raw_port.split(":")
if len(parts) != 2:
raise Exception(f"Invalid port definition: {raw_port}")
node_port = int(parts[0])
pod_port = int(parts[1])
else:
node_port = None
pod_port = int(raw_port)
service = client.V1Service( service = client.V1Service(
metadata=client.V1ObjectMeta(name=f"{self.app_name}-nodeport"), metadata=client.V1ObjectMeta(name=f"{self.app_name}-nodeport-{pod_port}"),
spec=client.V1ServiceSpec( spec=client.V1ServiceSpec(
type="NodePort", type="NodePort",
ports=[client.V1ServicePort( ports=[client.V1ServicePort(
port=port, port=pod_port,
target_port=port target_port=pod_port,
node_port=node_port
)], )],
selector={"app": self.app_name} selector={"app": self.app_name}
) )
) )
return service nodeports.append(service)
return nodeports
def get_ingress(self, use_tls=False, certificate=None, cluster_issuer="letsencrypt-prod"): def get_ingress(self, use_tls=False, certificate=None, cluster_issuer="letsencrypt-prod"):
# No ingress for a deployment that has no http-proxy defined, for now # No ingress for a deployment that has no http-proxy defined, for now
@ -248,12 +261,12 @@ class ClusterInfo:
for f in os.listdir(cfg_map_path): for f in os.listdir(cfg_map_path):
full_path = os.path.join(cfg_map_path, f) full_path = os.path.join(cfg_map_path, f)
if os.path.isfile(full_path): if os.path.isfile(full_path):
data[f] = open(full_path, 'rt').read() data[f] = base64.b64encode(open(full_path, 'rb').read()).decode('ASCII')
spec = client.V1ConfigMap( spec = client.V1ConfigMap(
metadata=client.V1ObjectMeta(name=f"{self.app_name}-{cfg_map_name}", metadata=client.V1ObjectMeta(name=f"{self.app_name}-{cfg_map_name}",
labels={"configmap-label": cfg_map_name}), labels={"configmap-label": cfg_map_name}),
data=data binary_data=data
) )
result.append(spec) result.append(spec)
return result return result
@ -353,6 +366,8 @@ class ClusterInfo:
annotations = None annotations = None
labels = {"app": self.app_name} labels = {"app": self.app_name}
affinity = None
tolerations = None
if self.spec.get_annotations(): if self.spec.get_annotations():
annotations = {} annotations = {}
@ -365,17 +380,60 @@ class ClusterInfo:
for service_name in services: for service_name in services:
labels[key.replace("{name}", service_name)] = value labels[key.replace("{name}", service_name)] = value
if self.spec.get_node_affinities():
affinities = []
for rule in self.spec.get_node_affinities():
# TODO add some input validation here
label_name = rule['label']
label_value = rule['value']
affinities.append(client.V1NodeSelectorTerm(
match_expressions=[client.V1NodeSelectorRequirement(
key=label_name,
operator="In",
values=[label_value]
)]
)
)
affinity = client.V1Affinity(
node_affinity=client.V1NodeAffinity(
required_during_scheduling_ignored_during_execution=client.V1NodeSelector(
node_selector_terms=affinities
))
)
if self.spec.get_node_tolerations():
tolerations = []
for toleration in self.spec.get_node_tolerations():
# TODO add some input validation here
toleration_key = toleration['key']
toleration_value = toleration['value']
tolerations.append(client.V1Toleration(
effect="NoSchedule",
key=toleration_key,
operator="Equal",
value=toleration_value
))
template = client.V1PodTemplateSpec( template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta( metadata=client.V1ObjectMeta(
annotations=annotations, annotations=annotations,
labels=labels labels=labels
), ),
spec=client.V1PodSpec(containers=containers, image_pull_secrets=image_pull_secrets, volumes=volumes), spec=client.V1PodSpec(
containers=containers,
image_pull_secrets=image_pull_secrets,
volumes=volumes,
affinity=affinity,
tolerations=tolerations
),
) )
spec = client.V1DeploymentSpec( spec = client.V1DeploymentSpec(
replicas=1, template=template, selector={ replicas=self.spec.get_replicas(),
template=template, selector={
"matchLabels": "matchLabels":
{"app": self.app_name}}) {"app": self.app_name}
}
)
deployment = client.V1Deployment( deployment = client.V1Deployment(
api_version="apps/v1", api_version="apps/v1",

View File

@ -16,6 +16,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from kubernetes import client, config from kubernetes import client, config
from typing import List
from stack_orchestrator import constants from stack_orchestrator import constants
from stack_orchestrator.deploy.deployer import Deployer, DeployerConfigGenerator from stack_orchestrator.deploy.deployer import Deployer, DeployerConfigGenerator
@ -51,12 +52,14 @@ class K8sDeployer(Deployer):
networking_api: client.NetworkingV1Api networking_api: client.NetworkingV1Api
k8s_namespace: str = "default" k8s_namespace: str = "default"
kind_cluster_name: str kind_cluster_name: str
skip_cluster_management: bool
cluster_info: ClusterInfo cluster_info: ClusterInfo
deployment_dir: Path deployment_dir: Path
deployment_context: DeploymentContext deployment_context: DeploymentContext
def __init__(self, type, deployment_context: DeploymentContext, compose_files, compose_project_name, compose_env_file) -> None: def __init__(self, type, deployment_context: DeploymentContext, compose_files, compose_project_name, compose_env_file) -> None:
self.type = type self.type = type
self.skip_cluster_management = False
# TODO: workaround pending refactoring above to cope with being created with a null deployment_context # TODO: workaround pending refactoring above to cope with being created with a null deployment_context
if deployment_context is None: if deployment_context is None:
return return
@ -182,6 +185,7 @@ class K8sDeployer(Deployer):
if len(host_parts) == 2: if len(host_parts) == 2:
host_as_wild = f"*.{host_parts[1]}" host_as_wild = f"*.{host_parts[1]}"
# TODO: resolve method deprecation below
now = datetime.utcnow().replace(tzinfo=timezone.utc) now = datetime.utcnow().replace(tzinfo=timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%S%z" fmt = "%Y-%m-%dT%H:%M:%S%z"
@ -202,15 +206,16 @@ class K8sDeployer(Deployer):
return cert return cert
return None return None
def up(self, detach, services): def up(self, detach, skip_cluster_management, services):
self.skip_cluster_management = skip_cluster_management
if not opts.o.dry_run: if not opts.o.dry_run:
if self.is_kind(): if self.is_kind() and not self.skip_cluster_management:
# Create the kind cluster # Create the kind cluster
create_cluster(self.kind_cluster_name, self.deployment_dir.joinpath(constants.kind_config_filename)) create_cluster(self.kind_cluster_name, self.deployment_dir.joinpath(constants.kind_config_filename))
# Ensure the referenced containers are copied into kind # Ensure the referenced containers are copied into kind
load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set) load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set)
self.connect_api() self.connect_api()
if self.is_kind(): if self.is_kind() and not self.skip_cluster_management:
# Now configure an ingress controller (not installed by default in kind) # Now configure an ingress controller (not installed by default in kind)
install_ingress_for_kind() install_ingress_for_kind()
# Wait for ingress to start (deployment provisioning will fail unless this is done) # Wait for ingress to start (deployment provisioning will fail unless this is done)
@ -246,8 +251,8 @@ class K8sDeployer(Deployer):
if opts.o.debug: if opts.o.debug:
print("No ingress configured") print("No ingress configured")
nodeport: client.V1Service = self.cluster_info.get_nodeport() nodeports: List[client.V1Service] = self.cluster_info.get_nodeports()
if nodeport: for nodeport in nodeports:
if opts.o.debug: if opts.o.debug:
print(f"Sending this nodeport: {nodeport}") print(f"Sending this nodeport: {nodeport}")
if not opts.o.dry_run: if not opts.o.dry_run:
@ -259,7 +264,8 @@ class K8sDeployer(Deployer):
print("NodePort created:") print("NodePort created:")
print(f"{nodeport_resp}") print(f"{nodeport_resp}")
def down(self, timeout, volumes): # noqa: C901 def down(self, timeout, volumes, skip_cluster_management): # noqa: C901
self.skip_cluster_management = skip_cluster_management
self.connect_api() self.connect_api()
# Delete the k8s objects # Delete the k8s objects
@ -342,10 +348,10 @@ class K8sDeployer(Deployer):
if opts.o.debug: if opts.o.debug:
print("No ingress to delete") print("No ingress to delete")
nodeport: client.V1Service = self.cluster_info.get_nodeport() nodeports: List[client.V1Service] = self.cluster_info.get_nodeports()
if nodeport: for nodeport in nodeports:
if opts.o.debug: if opts.o.debug:
print(f"Deleting this nodeport: {ingress}") print(f"Deleting this nodeport: {nodeport}")
try: try:
self.core_api.delete_namespaced_service( self.core_api.delete_namespaced_service(
namespace=self.k8s_namespace, namespace=self.k8s_namespace,
@ -357,7 +363,7 @@ class K8sDeployer(Deployer):
if opts.o.debug: if opts.o.debug:
print("No nodeport to delete") print("No nodeport to delete")
if self.is_kind(): if self.is_kind() and not self.skip_cluster_management:
# Destroy the kind cluster # Destroy the kind cluster
destroy_cluster(self.kind_cluster_name) destroy_cluster(self.kind_cluster_name)

View File

@ -117,6 +117,15 @@ class Spec:
def get_annotations(self): def get_annotations(self):
return self.obj.get(constants.annotations_key, {}) return self.obj.get(constants.annotations_key, {})
def get_replicas(self):
return self.obj.get(constants.replicas_key, 1)
def get_node_affinities(self):
return self.obj.get(constants.node_affinities_key, [])
def get_node_tolerations(self):
return self.obj.get(constants.node_tolerations_key, [])
def get_labels(self): def get_labels(self):
return self.obj.get(constants.labels_key, {}) return self.obj.get(constants.labels_key, {})

View File

@ -21,16 +21,28 @@ import sys
import tempfile import tempfile
import time import time
import uuid import uuid
import yaml
import click import click
import gnupg
from stack_orchestrator.deploy.images import remote_image_exists, add_tags_to_image from stack_orchestrator.deploy.images import remote_image_exists
from stack_orchestrator.deploy.webapp import deploy_webapp from stack_orchestrator.deploy.webapp import deploy_webapp
from stack_orchestrator.deploy.webapp.util import (LaconicRegistryClient, TimedLogger, from stack_orchestrator.deploy.webapp.util import (
build_container_image, push_container_image, AttrDict,
file_hash, deploy_to_k8s, publish_deployment, LaconicRegistryClient,
hostname_for_deployment_request, generate_hostname_for_app, TimedLogger,
match_owner, skip_by_tag) build_container_image,
push_container_image,
file_hash,
deploy_to_k8s,
publish_deployment,
hostname_for_deployment_request,
generate_hostname_for_app,
match_owner,
skip_by_tag,
confirm_payment,
)
def process_app_deployment_request( def process_app_deployment_request(
@ -45,12 +57,19 @@ def process_app_deployment_request(
image_registry, image_registry,
force_rebuild, force_rebuild,
fqdn_policy, fqdn_policy,
logger recreate_on_deploy,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
logger,
): ):
logger.log("BEGIN - process_app_deployment_request") logger.log("BEGIN - process_app_deployment_request")
# 1. look up application # 1. look up application
app = laconic.get_record(app_deployment_request.attributes.application, require=True) app = laconic.get_record(
app_deployment_request.attributes.application, require=True
)
logger.log(f"Retrieved app record {app_deployment_request.attributes.application}") logger.log(f"Retrieved app record {app_deployment_request.attributes.application}")
# 2. determine dns # 2. determine dns
@ -61,106 +80,178 @@ def process_app_deployment_request(
if "allow" == fqdn_policy or "preexisting" == fqdn_policy: if "allow" == fqdn_policy or "preexisting" == fqdn_policy:
fqdn = requested_name fqdn = requested_name
else: else:
raise Exception(f"{requested_name} is invalid: only unqualified hostnames are allowed.") raise Exception(
f"{requested_name} is invalid: only unqualified hostnames are allowed."
)
else: else:
fqdn = f"{requested_name}.{default_dns_suffix}" fqdn = f"{requested_name}.{default_dns_suffix}"
# Normalize case (just in case)
fqdn = fqdn.lower()
# 3. check ownership of existing dnsrecord vs this request # 3. check ownership of existing dnsrecord vs this request
dns_crn = f"{dns_record_namespace}/{fqdn}" dns_lrn = f"{dns_record_namespace}/{fqdn}"
dns_record = laconic.get_record(dns_crn) dns_record = laconic.get_record(dns_lrn)
if dns_record: if dns_record:
matched_owner = match_owner(app_deployment_request, dns_record) matched_owner = match_owner(app_deployment_request, dns_record)
if not matched_owner and dns_record.attributes.request: if not matched_owner and dns_record.attributes.request:
matched_owner = match_owner(app_deployment_request, laconic.get_record(dns_record.attributes.request, require=True)) matched_owner = match_owner(
app_deployment_request,
laconic.get_record(dns_record.attributes.request, require=True),
)
if matched_owner: if matched_owner:
logger.log(f"Matched DnsRecord ownership: {matched_owner}") logger.log(f"Matched DnsRecord ownership: {matched_owner}")
else: else:
raise Exception("Unable to confirm ownership of DnsRecord %s for request %s" % raise Exception(
(dns_crn, app_deployment_request.id)) "Unable to confirm ownership of DnsRecord %s for request %s"
% (dns_lrn, app_deployment_request.id)
)
elif "preexisting" == fqdn_policy: elif "preexisting" == fqdn_policy:
raise Exception(f"No pre-existing DnsRecord {dns_crn} could be found for request {app_deployment_request.id}.") raise Exception(
f"No pre-existing DnsRecord {dns_lrn} could be found for request {app_deployment_request.id}."
)
# 4. get build and runtime config from request # 4. get build and runtime config from request
env = {}
if app_deployment_request.attributes.config:
if "ref" in app_deployment_request.attributes.config:
with open(
f"{config_upload_dir}/{app_deployment_request.attributes.config.ref}",
"rb",
) as file:
record_owner = laconic.get_owner(app_deployment_request)
decrypted = gpg.decrypt_file(file, passphrase=private_key_passphrase)
parsed = AttrDict(yaml.safe_load(decrypted.data))
if record_owner not in parsed.authorized:
raise Exception(
f"{record_owner} not authorized to access config {app_deployment_request.attributes.config.ref}"
)
if "env" in parsed.config:
env.update(parsed.config.env)
if "env" in app_deployment_request.attributes.config:
env.update(app_deployment_request.attributes.config.env)
env_filename = None env_filename = None
if app_deployment_request.attributes.config and "env" in app_deployment_request.attributes.config: if env:
env_filename = tempfile.mktemp() env_filename = tempfile.mktemp()
with open(env_filename, 'w') as file: with open(env_filename, "w") as file:
for k, v in app_deployment_request.attributes.config["env"].items(): for k, v in env.items():
file.write("%s=%s\n" % (k, shlex.quote(str(v)))) file.write("%s=%s\n" % (k, shlex.quote(str(v))))
# 5. determine new or existing deployment # 5. determine new or existing deployment
# a. check for deployment crn # a. check for deployment lrn
app_deployment_crn = f"{deployment_record_namespace}/{fqdn}" app_deployment_lrn = f"{deployment_record_namespace}/{fqdn}"
if app_deployment_request.attributes.deployment: if app_deployment_request.attributes.deployment:
app_deployment_crn = app_deployment_request.attributes.deployment app_deployment_lrn = app_deployment_request.attributes.deployment
if not app_deployment_crn.startswith(deployment_record_namespace): if not app_deployment_lrn.startswith(deployment_record_namespace):
raise Exception("Deployment CRN %s is not in a supported namespace" % app_deployment_request.attributes.deployment) raise Exception(
"Deployment LRN %s is not in a supported namespace"
% app_deployment_request.attributes.deployment
)
deployment_record = laconic.get_record(app_deployment_crn) deployment_record = laconic.get_record(app_deployment_lrn)
deployment_dir = os.path.join(deployment_parent_dir, fqdn) deployment_dir = os.path.join(deployment_parent_dir, fqdn)
# At present we use this to generate a unique but stable ID for the app's host container
# TODO: implement support to derive this transparently from the already-unique deployment id
unique_deployment_id = hashlib.md5(fqdn.encode()).hexdigest()[:16]
deployment_config_file = os.path.join(deployment_dir, "config.env") deployment_config_file = os.path.join(deployment_dir, "config.env")
# TODO: Is there any reason not to simplify the hash input to the app_deployment_crn? deployment_container_tag = "laconic-webapp/%s:local" % unique_deployment_id
deployment_container_tag = "laconic-webapp/%s:local" % hashlib.md5(deployment_dir.encode()).hexdigest()
app_image_shared_tag = f"laconic-webapp/{app.id}:local" app_image_shared_tag = f"laconic-webapp/{app.id}:local"
# b. check for deployment directory (create if necessary) # b. check for deployment directory (create if necessary)
if not os.path.exists(deployment_dir): if not os.path.exists(deployment_dir):
if deployment_record: if deployment_record:
raise Exception("Deployment record %s exists, but not deployment dir %s. Please remove name." % raise Exception(
(app_deployment_crn, deployment_dir)) "Deployment record %s exists, but not deployment dir %s. Please remove name."
print("deploy_webapp", deployment_dir) % (app_deployment_lrn, deployment_dir)
deploy_webapp.create_deployment(ctx, deployment_dir, deployment_container_tag, )
f"https://{fqdn}", kube_config, image_registry, env_filename) logger.log(
f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}"
)
deploy_webapp.create_deployment(
ctx,
deployment_dir,
deployment_container_tag,
f"https://{fqdn}",
kube_config,
image_registry,
env_filename,
)
elif env_filename: elif env_filename:
shutil.copyfile(env_filename, deployment_config_file) shutil.copyfile(env_filename, deployment_config_file)
needs_k8s_deploy = False needs_k8s_deploy = False
if force_rebuild:
logger.log(
"--force-rebuild is enabled so the container will always be built now, even if nothing has changed in the app"
)
# 6. build container (if needed) # 6. build container (if needed)
if not deployment_record or deployment_record.attributes.application != app.id: # TODO: add a comment that explains what this code is doing (not clear to me)
if (
not deployment_record
or deployment_record.attributes.application != app.id
or force_rebuild
):
needs_k8s_deploy = True needs_k8s_deploy = True
# check if the image already exists # check if the image already exists
shared_tag_exists = remote_image_exists(image_registry, app_image_shared_tag) shared_tag_exists = remote_image_exists(image_registry, app_image_shared_tag)
# Note: in the code below, calls to add_tags_to_image() won't work at present.
# This is because SO deployment code in general re-names the container image
# to be unique to the deployment. This is done transparently
# and so when we call add_tags_to_image() here and try to add tags to the remote image,
# we get the image name wrong. Accordingly I've disabled the relevant code for now.
# This is safe because we are running with --force-rebuild at present
if shared_tag_exists and not force_rebuild: if shared_tag_exists and not force_rebuild:
# simply add our unique tag to the existing image and we are done # simply add our unique tag to the existing image and we are done
logger.log(f"Using existing app image {app_image_shared_tag} for {deployment_container_tag}") logger.log(
add_tags_to_image(image_registry, app_image_shared_tag, deployment_container_tag) f"(SKIPPED) Existing image found for this app: {app_image_shared_tag} "
"tagging it with: {deployment_container_tag} to use in this deployment"
)
# add_tags_to_image(image_registry, app_image_shared_tag, deployment_container_tag)
logger.log("Tag complete") logger.log("Tag complete")
else: else:
extra_build_args = [] # TODO: pull from request extra_build_args = [] # TODO: pull from request
logger.log(f"Building container image {deployment_container_tag}") logger.log(f"Building container image: {deployment_container_tag}")
build_container_image(app, deployment_container_tag, extra_build_args, logger) build_container_image(
app, deployment_container_tag, extra_build_args, logger
)
logger.log("Build complete") logger.log("Build complete")
logger.log(f"Pushing container image {deployment_container_tag}") logger.log(f"Pushing container image: {deployment_container_tag}")
push_container_image(deployment_dir, logger) push_container_image(deployment_dir, logger)
logger.log("Push complete") logger.log("Push complete")
# The build/push commands above will use the unique deployment tag, so now we need to add the shared tag. # The build/push commands above will use the unique deployment tag, so now we need to add the shared tag.
logger.log(f"Updating app image tag {app_image_shared_tag} from build of {deployment_container_tag}") logger.log(
add_tags_to_image(image_registry, deployment_container_tag, app_image_shared_tag) f"(SKIPPED) Adding global app image tag: {app_image_shared_tag} to newly built image: {deployment_container_tag}"
)
# add_tags_to_image(image_registry, deployment_container_tag, app_image_shared_tag)
logger.log("Tag complete") logger.log("Tag complete")
else:
logger.log("Requested app is already deployed, skipping build and image push")
# 7. update config (if needed) # 7. update config (if needed)
if not deployment_record or file_hash(deployment_config_file) != deployment_record.attributes.meta.config: if (
not deployment_record
or file_hash(deployment_config_file) != deployment_record.attributes.meta.config
):
needs_k8s_deploy = True needs_k8s_deploy = True
# 8. update k8s deployment # 8. update k8s deployment
if needs_k8s_deploy: if needs_k8s_deploy:
deploy_to_k8s( deploy_to_k8s(deployment_record, deployment_dir, recreate_on_deploy, logger)
deployment_record,
deployment_dir,
logger
)
logger.log("Publishing deployment to registry.") logger.log("Publishing deployment to registry.")
publish_deployment( publish_deployment(
laconic, laconic,
app, app,
deployment_record, deployment_record,
app_deployment_crn, app_deployment_lrn,
dns_record, dns_record,
dns_crn, dns_lrn,
deployment_dir, deployment_dir,
app_deployment_request, app_deployment_request,
logger webapp_deployer_record,
logger,
) )
logger.log("Publication complete.") logger.log("Publication complete.")
logger.log("END - process_app_deployment_request") logger.log("END - process_app_deployment_request")
@ -177,37 +268,129 @@ def dump_known_requests(filename, requests, status="SEEN"):
return return
known_requests = load_known_requests(filename) known_requests = load_known_requests(filename)
for r in requests: for r in requests:
known_requests[r.id] = { known_requests[r.id] = {"createTime": r.createTime, "status": status}
"createTime": r.createTime,
"status": status
}
with open(filename, "w") as f: with open(filename, "w") as f:
json.dump(known_requests, f) json.dump(known_requests, f)
@click.command() @click.command()
@click.option("--kube-config", help="Provide a config file for a k8s deployment") @click.option("--kube-config", help="Provide a config file for a k8s deployment")
@click.option("--laconic-config", help="Provide a config file for laconicd", required=True) @click.option(
@click.option("--image-registry", help="Provide a container image registry url for this k8s cluster") "--laconic-config", help="Provide a config file for laconicd", required=True
@click.option("--deployment-parent-dir", help="Create deployment directories beneath this directory", required=True) )
@click.option(
"--image-registry",
help="Provide a container image registry url for this k8s cluster",
)
@click.option(
"--deployment-parent-dir",
help="Create deployment directories beneath this directory",
required=True,
)
@click.option("--request-id", help="The ApplicationDeploymentRequest to process") @click.option("--request-id", help="The ApplicationDeploymentRequest to process")
@click.option("--discover", help="Discover and process all pending ApplicationDeploymentRequests", is_flag=True, default=False) @click.option(
@click.option("--state-file", help="File to store state about previously seen requests.") "--discover",
@click.option("--only-update-state", help="Only update the state file, don't process any requests anything.", is_flag=True) help="Discover and process all pending ApplicationDeploymentRequests",
is_flag=True,
default=False,
)
@click.option(
"--state-file", help="File to store state about previously seen requests."
)
@click.option(
"--only-update-state",
help="Only update the state file, don't process any requests anything.",
is_flag=True,
)
@click.option("--dns-suffix", help="DNS domain to use eg, laconic.servesthe.world") @click.option("--dns-suffix", help="DNS domain to use eg, laconic.servesthe.world")
@click.option("--fqdn-policy", help="How to handle requests with an FQDN: prohibit, allow, preexisting", default="prohibit") @click.option(
@click.option("--record-namespace-dns", help="eg, crn://laconic/dns") "--fqdn-policy",
@click.option("--record-namespace-deployments", help="eg, crn://laconic/deployments") help="How to handle requests with an FQDN: prohibit, allow, preexisting",
@click.option("--dry-run", help="Don't do anything, just report what would be done.", is_flag=True) default="prohibit",
@click.option("--include-tags", help="Only include requests with matching tags (comma-separated).", default="") )
@click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="") @click.option("--record-namespace-dns", help="eg, lrn://laconic/dns", required=True)
@click.option("--force-rebuild", help="Rebuild even if the image already exists.", is_flag=True) @click.option(
@click.option("--log-dir", help="Output build/deployment logs to directory.", default=None) "--record-namespace-deployments",
help="eg, lrn://laconic/deployments",
required=True,
)
@click.option(
"--dry-run", help="Don't do anything, just report what would be done.", is_flag=True
)
@click.option(
"--include-tags",
help="Only include requests with matching tags (comma-separated).",
default="",
)
@click.option(
"--exclude-tags",
help="Exclude requests with matching tags (comma-separated).",
default="",
)
@click.option(
"--force-rebuild", help="Rebuild even if the image already exists.", is_flag=True
)
@click.option(
"--recreate-on-deploy",
help="Remove and recreate deployments instead of updating them.",
is_flag=True,
)
@click.option(
"--log-dir", help="Output build/deployment logs to directory.", default=None
)
@click.option(
"--min-required-payment",
help="Requests must have a minimum payment to be processed (in alnt)",
default=0,
)
@click.option("--lrn", help="The LRN of this deployer.", required=True)
@click.option(
"--all-requests",
help="Handle requests addressed to anyone (by default only requests to"
"my payment address are examined).",
is_flag=True,
)
@click.option(
"--config-upload-dir",
help="The directory containing uploaded config.",
required=True,
)
@click.option(
"--private-key-file", help="The private key for decrypting config.", required=True
)
@click.option(
"--private-key-passphrase",
help="The passphrase for the private key.",
required=True,
)
@click.pass_context @click.pass_context
def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_dir, # noqa: C901 def command( # noqa: C901
request_id, discover, state_file, only_update_state, ctx,
dns_suffix, fqdn_policy, record_namespace_dns, record_namespace_deployments, dry_run, kube_config,
include_tags, exclude_tags, force_rebuild, log_dir): laconic_config,
image_registry,
deployment_parent_dir,
request_id,
discover,
state_file,
only_update_state,
dns_suffix,
fqdn_policy,
record_namespace_dns,
record_namespace_deployments,
dry_run,
include_tags,
exclude_tags,
force_rebuild,
recreate_on_deploy,
log_dir,
min_required_payment,
lrn,
config_upload_dir,
private_key_file,
private_key_passphrase,
all_requests,
):
if request_id and discover: if request_id and discover:
print("Cannot specify both --request-id and --discover", file=sys.stderr) print("Cannot specify both --request-id and --discover", file=sys.stderr)
sys.exit(2) sys.exit(2)
@ -221,33 +404,76 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_
sys.exit(2) sys.exit(2)
if not only_update_state: if not only_update_state:
if not record_namespace_dns or not record_namespace_deployments or not dns_suffix: if (
print("--dns-suffix, --record-namespace-dns, and --record-namespace-deployments are all required", file=sys.stderr) not record_namespace_dns
or not record_namespace_deployments
or not dns_suffix
):
print(
"--dns-suffix, --record-namespace-dns, and --record-namespace-deployments are all required",
file=sys.stderr,
)
sys.exit(2) sys.exit(2)
if fqdn_policy not in ["prohibit", "allow", "preexisting"]: if fqdn_policy not in ["prohibit", "allow", "preexisting"]:
print("--fqdn-policy must be one of 'prohibit', 'allow', or 'preexisting'", file=sys.stderr) print(
"--fqdn-policy must be one of 'prohibit', 'allow', or 'preexisting'",
file=sys.stderr,
)
sys.exit(2) sys.exit(2)
tempdir = tempfile.mkdtemp()
gpg = gnupg.GPG(gnupghome=tempdir)
# Import the deployer's public key
result = gpg.import_keys(open(private_key_file, "rb").read())
if 1 != result.imported:
print(
f"Failed to load private key file: {private_key_file}.",
file=sys.stderr,
)
sys.exit(2)
main_logger = TimedLogger(file=sys.stderr)
try:
# Split CSV and clean up values. # Split CSV and clean up values.
include_tags = [tag.strip() for tag in include_tags.split(",") if tag] include_tags = [tag.strip() for tag in include_tags.split(",") if tag]
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag] exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
laconic = LaconicRegistryClient(laconic_config) laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
webapp_deployer_record = laconic.get_record(lrn, require=True)
payment_address = webapp_deployer_record.attributes.paymentAddress
main_logger.log(f"Payment address: {payment_address}")
if min_required_payment and not payment_address:
print(
f"Minimum payment required, but no payment address listed for deployer: {lrn}.",
file=sys.stderr,
)
sys.exit(2)
# Find deployment requests. # Find deployment requests.
# single request # single request
if request_id: if request_id:
main_logger.log(f"Retrieving request {request_id}...")
requests = [laconic.get_record(request_id, require=True)] requests = [laconic.get_record(request_id, require=True)]
# all requests # all requests
elif discover: elif discover:
main_logger.log("Discovering deployment requests...")
if all_requests:
requests = laconic.app_deployment_requests() requests = laconic.app_deployment_requests()
else:
requests = laconic.app_deployment_requests({"deployer": lrn})
if only_update_state: if only_update_state:
if not dry_run: if not dry_run:
dump_known_requests(state_file, requests) dump_known_requests(state_file, requests)
return return
previous_requests = {}
if state_file:
main_logger.log(f"Loading known requests from {state_file}...")
previous_requests = load_known_requests(state_file) previous_requests = load_known_requests(state_file)
# Collapse related requests. # Collapse related requests.
@ -256,68 +482,133 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_
requests_by_name = {} requests_by_name = {}
skipped_by_name = {} skipped_by_name = {}
for r in requests: for r in requests:
if r.id in previous_requests and previous_requests[r.id].get("status", "") != "RETRY": main_logger.log(f"BEGIN: Examining request {r.id}")
print(f"Skipping request {r.id}, we've already seen it.") result = "PENDING"
try:
if (
r.id in previous_requests
and previous_requests[r.id].get("status", "") != "RETRY"
):
main_logger.log(f"Skipping request {r.id}, we've already seen it.")
result = "SKIP"
continue continue
app = laconic.get_record(r.attributes.application) app = laconic.get_record(r.attributes.application)
if not app: if not app:
print("Skipping request %s, cannot locate app." % r.id) main_logger.log(f"Skipping request {r.id}, cannot locate app.")
result = "ERROR"
continue continue
requested_name = r.attributes.dns requested_name = r.attributes.dns
if not requested_name: if not requested_name:
requested_name = generate_hostname_for_app(app) requested_name = generate_hostname_for_app(app)
print("Generating name %s for request %s." % (requested_name, r.id)) main_logger.log(
"Generating name %s for request %s." % (requested_name, r.id)
)
if requested_name in skipped_by_name or requested_name in requests_by_name: if (
print("Ignoring request %s, it has been superseded." % r.id) requested_name in skipped_by_name
or requested_name in requests_by_name
):
main_logger.log(
"Ignoring request %s, it has been superseded." % r.id
)
result = "SKIP"
continue continue
if skip_by_tag(r, include_tags, exclude_tags): if skip_by_tag(r, include_tags, exclude_tags):
print("Skipping request %s, filtered by tag (include %s, exclude %s, present %s)" % (r.id, main_logger.log(
include_tags, "Skipping request %s, filtered by tag (include %s, exclude %s, present %s)"
exclude_tags, % (r.id, include_tags, exclude_tags, r.attributes.tags)
r.attributes.tags)) )
skipped_by_name[requested_name] = r skipped_by_name[requested_name] = r
result = "SKIP"
continue continue
print("Found request %s to run application %s on %s." % (r.id, r.attributes.application, requested_name)) main_logger.log(
"Found pending request %s to run application %s on %s."
% (r.id, r.attributes.application, requested_name)
)
requests_by_name[requested_name] = r requests_by_name[requested_name] = r
except Exception as e:
result = "ERROR"
main_logger.log(f"ERROR examining request {r.id}: " + str(e))
finally:
main_logger.log(f"DONE Examining request {r.id} with result {result}.")
if result in ["ERROR"]:
dump_known_requests(state_file, [r], status=result)
# Find deployments. # Find deployments.
main_logger.log("Discovering existing app deployments...")
if all_requests:
deployments = laconic.app_deployments() deployments = laconic.app_deployments()
else:
deployments = laconic.app_deployments({"deployer": lrn})
deployments_by_request = {} deployments_by_request = {}
for d in deployments: for d in deployments:
if d.attributes.request: if d.attributes.request:
deployments_by_request[d.attributes.request] = d deployments_by_request[d.attributes.request] = d
# Find removal requests. # Find removal requests.
main_logger.log("Discovering deployment removal and cancellation requests...")
cancellation_requests = {} cancellation_requests = {}
removal_requests = laconic.app_deployment_removal_requests() removal_requests = laconic.app_deployment_removal_requests()
for r in removal_requests: for r in removal_requests:
if r.attributes.request: if r.attributes.request:
cancellation_requests[r.attributes.request] = r cancellation_requests[r.attributes.request] = r
requests_to_execute = [] requests_to_check_for_payment = []
for r in requests_by_name.values(): for r in requests_by_name.values():
if r.id in cancellation_requests and match_owner(cancellation_requests[r.id], r): if r.id in cancellation_requests and match_owner(
print(f"Found deployment cancellation request for {r.id} at {cancellation_requests[r.id].id}") cancellation_requests[r.id], r
):
main_logger.log(
f"Found deployment cancellation request for {r.id} at {cancellation_requests[r.id].id}"
)
elif r.id in deployments_by_request: elif r.id in deployments_by_request:
print(f"Found satisfied request for {r.id} at {deployments_by_request[r.id].id}") main_logger.log(
f"Found satisfied request for {r.id} at {deployments_by_request[r.id].id}"
)
else: else:
if r.id not in previous_requests: if (
print(f"Request {r.id} needs to processed.") r.id in previous_requests
requests_to_execute.append(r) and previous_requests[r.id].get("status", "") != "RETRY"
else: ):
print( main_logger.log(
f"Skipping unsatisfied request {r.id} because we have seen it before." f"Skipping unsatisfied request {r.id} because we have seen it before."
) )
else:
main_logger.log(f"Request {r.id} needs to processed.")
requests_to_check_for_payment.append(r)
print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) requests_to_execute = []
if min_required_payment:
for r in requests_to_check_for_payment:
main_logger.log(f"{r.id}: Confirming payment...")
if confirm_payment(
laconic,
r,
payment_address,
min_required_payment,
main_logger,
):
main_logger.log(f"{r.id}: Payment confirmed.")
requests_to_execute.append(r)
else:
main_logger.log(
f"Skipping request {r.id}: unable to verify payment."
)
dump_known_requests(state_file, [r], status="UNPAID")
else:
requests_to_execute = requests_to_check_for_payment
main_logger.log(
"Found %d unsatisfied request(s) to process." % len(requests_to_execute)
)
if not dry_run: if not dry_run:
for r in requests_to_execute: for r in requests_to_execute:
main_logger.log(f"DEPLOYING {r.id}: BEGIN")
dump_known_requests(state_file, [r], "DEPLOYING") dump_known_requests(state_file, [r], "DEPLOYING")
status = "ERROR" status = "ERROR"
run_log_file = None run_log_file = None
@ -329,12 +620,16 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_
if not os.path.exists(run_log_dir): if not os.path.exists(run_log_dir):
os.mkdir(run_log_dir) os.mkdir(run_log_dir)
run_log_file_path = os.path.join(run_log_dir, f"{run_id}.log") run_log_file_path = os.path.join(run_log_dir, f"{run_id}.log")
print(f"Directing deployment logs to: {run_log_file_path}") main_logger.log(
f"Directing deployment logs to: {run_log_file_path}"
)
run_log_file = open(run_log_file_path, "wt") run_log_file = open(run_log_file_path, "wt")
run_reg_client = LaconicRegistryClient(laconic_config, log_file=run_log_file) run_reg_client = LaconicRegistryClient(
laconic_config, log_file=run_log_file
)
logger = TimedLogger(run_id, run_log_file) build_logger = TimedLogger(run_id, run_log_file)
logger.log("Processing ...") build_logger.log("Processing ...")
process_app_deployment_request( process_app_deployment_request(
ctx, ctx,
run_reg_client, run_reg_client,
@ -347,14 +642,30 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_
image_registry, image_registry,
force_rebuild, force_rebuild,
fqdn_policy, fqdn_policy,
logger recreate_on_deploy,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
build_logger,
) )
status = "DEPLOYED" status = "DEPLOYED"
except Exception as e: except Exception as e:
logger.log("ERROR: " + str(e)) main_logger.log(f"ERROR {r.id}:" + str(e))
build_logger.log("ERROR: " + str(e))
finally: finally:
if logger: main_logger.log(f"DEPLOYING {r.id}: END - {status}")
logger.log(f"DONE with status {status}", show_step_time=False, show_total_time=True) if build_logger:
build_logger.log(
f"DONE with status {status}",
show_step_time=False,
show_total_time=True,
)
dump_known_requests(state_file, [r], status) dump_known_requests(state_file, [r], status)
if run_log_file: if run_log_file:
run_log_file.close() run_log_file.close()
except Exception as e:
main_logger.log("UNCAUGHT ERROR:" + str(e))
raise e
finally:
shutil.rmtree(tempdir, ignore_errors=True)

View File

@ -0,0 +1,91 @@
# Copyright ©2023 Vulcanize
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
import base64
import click
import sys
import yaml
from urllib.parse import urlparse
from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient
@click.command()
@click.option(
"--laconic-config", help="Provide a config file for laconicd", required=True
)
@click.option("--api-url", help="The API URL of the deployer.", required=True)
@click.option(
"--public-key-file",
help="The public key to use. This should be a binary file.",
required=True,
)
@click.option(
"--lrn", help="eg, lrn://laconic/deployers/my.deployer.name", required=True
)
@click.option(
"--payment-address",
help="The address to which payments should be made. "
"Default is the current laconic account.",
default=None,
)
@click.option(
"--min-required-payment",
help="List the minimum required payment (in alnt) to process a deployment request.",
default=0,
)
@click.option(
"--dry-run",
help="Don't publish anything, just report what would be done.",
is_flag=True,
)
@click.pass_context
def command( # noqa: C901
ctx,
laconic_config,
api_url,
public_key_file,
lrn,
payment_address,
min_required_payment,
dry_run,
):
laconic = LaconicRegistryClient(laconic_config)
if not payment_address:
payment_address = laconic.whoami().address
pub_key = base64.b64encode(open(public_key_file, "rb").read()).decode("ASCII")
hostname = urlparse(api_url).hostname
webapp_deployer_record = {
"record": {
"type": "WebappDeployer",
"version": "1.0.0",
"apiUrl": api_url,
"name": hostname,
"publicKey": pub_key,
"paymentAddress": payment_address,
}
}
if min_required_payment:
webapp_deployer_record["record"][
"minimumPayment"
] = f"{min_required_payment}alnt"
if dry_run:
yaml.dump(webapp_deployer_record, sys.stdout)
return
laconic.publish(webapp_deployer_record, [lrn])

View File

@ -0,0 +1,175 @@
# Copyright ©2023 Vulcanize
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
import base64
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
import shutil
import sys
import tempfile
from datetime import datetime
import gnupg
import click
import requests
import yaml
from stack_orchestrator.deploy.webapp.util import (
LaconicRegistryClient,
)
from dotenv import dotenv_values
def fatal(msg: str):
print(msg, file=sys.stderr)
sys.exit(1)
@click.command()
@click.option(
"--laconic-config", help="Provide a config file for laconicd", required=True
)
@click.option(
"--app",
help="The LRN of the application to deploy.",
required=True,
)
@click.option(
"--deployer",
help="The LRN of the deployer to process this request.",
required=True,
)
@click.option("--env-file", help="environment file for webapp")
@click.option("--config-ref", help="The ref of an existing config upload to use.")
@click.option(
"--make-payment",
help="The payment to make (in alnt). The value should be a number or 'auto' to use the deployer's minimum required payment.",
)
@click.option(
"--use-payment", help="The TX id of an existing, unused payment", default=None
)
@click.option("--dns", help="the DNS name to request (default is autogenerated)")
@click.option(
"--dry-run",
help="Don't publish anything, just report what would be done.",
is_flag=True,
)
@click.pass_context
def command(
ctx,
laconic_config,
app,
deployer,
env_file,
config_ref,
make_payment,
use_payment,
dns,
dry_run,
): # noqa: C901
tempdir = tempfile.mkdtemp()
try:
laconic = LaconicRegistryClient(laconic_config)
app_record = laconic.get_record(app)
if not app_record:
fatal(f"Unable to locate app: {app}")
deployer_record = laconic.get_record(deployer)
if not deployer_record:
fatal(f"Unable to locate deployer: {deployer}")
if env_file and config_ref:
fatal("Cannot use --env-file and --config-ref at the same time.")
# If env_file
if env_file:
gpg = gnupg.GPG(gnupghome=tempdir)
# Import the deployer's public key
result = gpg.import_keys(
base64.b64decode(deployer_record.attributes.publicKey)
)
if 1 != result.imported:
fatal("Failed to import deployer's public key.")
recip = gpg.list_keys()[0]["uids"][0]
# Wrap the config
config = {
# Include account (and payment?) details
"authorized": [laconic.whoami().address],
"config": {"env": dict(dotenv_values(env_file))},
}
serialized = yaml.dump(config)
# Encrypt
result = gpg.encrypt(serialized, recip, always_trust=True, armor=False)
if not result.ok:
fatal("Failed to encrypt config.")
# Upload it to the deployer's API
response = requests.post(
f"{deployer_record.attributes.apiUrl}/upload/config",
data=result.data,
headers={"Content-Type": "application/octet-stream"},
)
if not response.ok:
response.raise_for_status()
config_ref = response.json()["id"]
deployment_request = {
"record": {
"type": "ApplicationDeploymentRequest",
"application": app,
"version": "1.0.0",
"name": f"{app_record.attributes.name}@{app_record.attributes.version}",
"deployer": deployer,
"meta": {"when": str(datetime.utcnow())},
}
}
if config_ref:
deployment_request["record"]["config"] = {"ref": config_ref}
if dns:
deployment_request["record"]["dns"] = dns.lower()
if make_payment:
amount = 0
if dry_run:
deployment_request["record"]["payment"] = "DRY_RUN"
elif "auto" == make_payment:
if "minimumPayment" in deployer_record.attributes:
amount = int(
deployer_record.attributes.minimumPayment.replace("alnt", "")
)
else:
amount = make_payment
if amount:
receipt = laconic.send_tokens(
deployer_record.attributes.paymentAddress, amount
)
deployment_request["record"]["payment"] = receipt.tx.hash
print("Payment TX:", receipt.tx.hash)
elif use_payment:
deployment_request["record"]["payment"] = use_payment
if dry_run:
print(yaml.dump(deployment_request))
return
# Send the request
laconic.publish(deployment_request)
finally:
shutil.rmtree(tempdir, ignore_errors=True)

View File

@ -20,18 +20,33 @@ import sys
import click import click
from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient, match_owner, skip_by_tag from stack_orchestrator.deploy.webapp.util import (
TimedLogger,
LaconicRegistryClient,
match_owner,
skip_by_tag,
confirm_payment,
)
main_logger = TimedLogger(file=sys.stderr)
def process_app_removal_request(ctx, def process_app_removal_request(
ctx,
laconic: LaconicRegistryClient, laconic: LaconicRegistryClient,
app_removal_request, app_removal_request,
deployment_parent_dir, deployment_parent_dir,
delete_volumes, delete_volumes,
delete_names): delete_names,
deployment_record = laconic.get_record(app_removal_request.attributes.deployment, require=True) webapp_deployer_record,
):
deployment_record = laconic.get_record(
app_removal_request.attributes.deployment, require=True
)
dns_record = laconic.get_record(deployment_record.attributes.dns, require=True) dns_record = laconic.get_record(deployment_record.attributes.dns, require=True)
deployment_dir = os.path.join(deployment_parent_dir, dns_record.attributes.name) deployment_dir = os.path.join(
deployment_parent_dir, dns_record.attributes.name.lower()
)
if not os.path.exists(deployment_dir): if not os.path.exists(deployment_dir):
raise Exception("Deployment directory %s does not exist." % deployment_dir) raise Exception("Deployment directory %s does not exist." % deployment_dir)
@ -41,13 +56,18 @@ def process_app_removal_request(ctx,
# Or of the original deployment request. # Or of the original deployment request.
if not matched_owner and deployment_record.attributes.request: if not matched_owner and deployment_record.attributes.request:
matched_owner = match_owner(app_removal_request, laconic.get_record(deployment_record.attributes.request, require=True)) matched_owner = match_owner(
app_removal_request,
laconic.get_record(deployment_record.attributes.request, require=True),
)
if matched_owner: if matched_owner:
print("Matched deployment ownership:", matched_owner) main_logger.log("Matched deployment ownership:", matched_owner)
else: else:
raise Exception("Unable to confirm ownership of deployment %s for removal request %s" % raise Exception(
(deployment_record.id, app_removal_request.id)) "Unable to confirm ownership of deployment %s for removal request %s"
% (deployment_record.id, app_removal_request.id)
)
# TODO(telackey): Call the function directly. The easiest way to build the correct click context is to # TODO(telackey): Call the function directly. The easiest way to build the correct click context is to
# exec the process, but it would be better to refactor so we could just call down_operation with the # exec the process, but it would be better to refactor so we could just call down_operation with the
@ -64,8 +84,13 @@ def process_app_removal_request(ctx,
"version": "1.0.0", "version": "1.0.0",
"request": app_removal_request.id, "request": app_removal_request.id,
"deployment": deployment_record.id, "deployment": deployment_record.id,
"deployer": webapp_deployer_record.names[0],
} }
} }
if app_removal_request.attributes.payment:
removal_record["record"]["payment"] = app_removal_request.attributes.payment
laconic.publish(removal_record) laconic.publish(removal_record)
if delete_names: if delete_names:
@ -97,22 +122,80 @@ def dump_known_requests(filename, requests):
@click.command() @click.command()
@click.option("--laconic-config", help="Provide a config file for laconicd", required=True) @click.option(
@click.option("--deployment-parent-dir", help="Create deployment directories beneath this directory", required=True) "--laconic-config", help="Provide a config file for laconicd", required=True
)
@click.option(
"--deployment-parent-dir",
help="Create deployment directories beneath this directory",
required=True,
)
@click.option("--request-id", help="The ApplicationDeploymentRemovalRequest to process") @click.option("--request-id", help="The ApplicationDeploymentRemovalRequest to process")
@click.option("--discover", help="Discover and process all pending ApplicationDeploymentRemovalRequests", @click.option(
is_flag=True, default=False) "--discover",
@click.option("--state-file", help="File to store state about previously seen requests.") help="Discover and process all pending ApplicationDeploymentRemovalRequests",
@click.option("--only-update-state", help="Only update the state file, don't process any requests anything.", is_flag=True) is_flag=True,
@click.option("--delete-names/--preserve-names", help="Delete all names associated with removed deployments.", default=True) default=False,
@click.option("--delete-volumes/--preserve-volumes", default=True, help="delete data volumes") )
@click.option("--dry-run", help="Don't do anything, just report what would be done.", is_flag=True) @click.option(
@click.option("--include-tags", help="Only include requests with matching tags (comma-separated).", default="") "--state-file", help="File to store state about previously seen requests."
@click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="") )
@click.option(
"--only-update-state",
help="Only update the state file, don't process any requests anything.",
is_flag=True,
)
@click.option(
"--delete-names/--preserve-names",
help="Delete all names associated with removed deployments.",
default=True,
)
@click.option(
"--delete-volumes/--preserve-volumes", default=True, help="delete data volumes"
)
@click.option(
"--dry-run", help="Don't do anything, just report what would be done.", is_flag=True
)
@click.option(
"--include-tags",
help="Only include requests with matching tags (comma-separated).",
default="",
)
@click.option(
"--exclude-tags",
help="Exclude requests with matching tags (comma-separated).",
default="",
)
@click.option(
"--min-required-payment",
help="Requests must have a minimum payment to be processed (in alnt)",
default=0,
)
@click.option("--lrn", help="The LRN of this deployer.", required=True)
@click.option(
"--all-requests",
help="Handle requests addressed to anyone (by default only requests to"
"my payment address are examined).",
is_flag=True,
)
@click.pass_context @click.pass_context
def command(ctx, laconic_config, deployment_parent_dir, def command( # noqa: C901
request_id, discover, state_file, only_update_state, ctx,
delete_names, delete_volumes, dry_run, include_tags, exclude_tags): laconic_config,
deployment_parent_dir,
request_id,
discover,
state_file,
only_update_state,
delete_names,
delete_volumes,
dry_run,
include_tags,
exclude_tags,
min_required_payment,
lrn,
all_requests,
):
if request_id and discover: if request_id and discover:
print("Cannot specify both --request-id and --discover", file=sys.stderr) print("Cannot specify both --request-id and --discover", file=sys.stderr)
sys.exit(2) sys.exit(2)
@ -129,34 +212,55 @@ def command(ctx, laconic_config, deployment_parent_dir,
include_tags = [tag.strip() for tag in include_tags.split(",") if tag] include_tags = [tag.strip() for tag in include_tags.split(",") if tag]
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag] exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
laconic = LaconicRegistryClient(laconic_config) laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
deployer_record = laconic.get_record(lrn, require=True)
payment_address = deployer_record.attributes.paymentAddress
main_logger.log(f"Payment address: {payment_address}")
if min_required_payment and not payment_address:
print(
f"Minimum payment required, but no payment address listed for deployer: {lrn}.",
file=sys.stderr,
)
sys.exit(2)
# Find deployment removal requests. # Find deployment removal requests.
# single request # single request
if request_id: if request_id:
main_logger.log(f"Retrieving request {request_id}...")
requests = [laconic.get_record(request_id, require=True)] requests = [laconic.get_record(request_id, require=True)]
# TODO: assert record type # TODO: assert record type
# all requests # all requests
elif discover: elif discover:
main_logger.log("Discovering removal requests...")
if all_requests:
requests = laconic.app_deployment_removal_requests() requests = laconic.app_deployment_removal_requests()
else:
requests = laconic.app_deployment_removal_requests({"deployer": lrn})
if only_update_state: if only_update_state:
if not dry_run: if not dry_run:
dump_known_requests(state_file, requests) dump_known_requests(state_file, requests)
return return
previous_requests = {}
if state_file:
main_logger.log(f"Loading known requests from {state_file}...")
previous_requests = load_known_requests(state_file) previous_requests = load_known_requests(state_file)
requests.sort(key=lambda r: r.createTime) requests.sort(key=lambda r: r.createTime)
requests.reverse() requests.reverse()
# Find deployments. # Find deployments.
deployments = {} named_deployments = {}
for d in laconic.app_deployments(all=True): main_logger.log("Discovering app deployments...")
deployments[d.id] = d for d in laconic.app_deployments(all=False):
named_deployments[d.id] = d
# Find removal requests. # Find removal requests.
removals_by_deployment = {} removals_by_deployment = {}
removals_by_request = {} removals_by_request = {}
main_logger.log("Discovering deployment removals...")
for r in laconic.app_deployment_removals(): for r in laconic.app_deployment_removals():
if r.attributes.deployment: if r.attributes.deployment:
# TODO: should we handle CRNs? # TODO: should we handle CRNs?
@ -165,33 +269,69 @@ def command(ctx, laconic_config, deployment_parent_dir,
one_per_deployment = {} one_per_deployment = {}
for r in requests: for r in requests:
if not r.attributes.deployment: if not r.attributes.deployment:
print(f"Skipping removal request {r.id} since it was a cancellation.") main_logger.log(
f"Skipping removal request {r.id} since it was a cancellation."
)
elif r.attributes.deployment in one_per_deployment: elif r.attributes.deployment in one_per_deployment:
print(f"Skipping removal request {r.id} since it was superseded.") main_logger.log(f"Skipping removal request {r.id} since it was superseded.")
else: else:
one_per_deployment[r.attributes.deployment] = r one_per_deployment[r.attributes.deployment] = r
requests_to_execute = [] requests_to_check_for_payment = []
for r in one_per_deployment.values(): for r in one_per_deployment.values():
if skip_by_tag(r, include_tags, exclude_tags): try:
print("Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)" % (r.id, if r.attributes.deployment not in named_deployments:
include_tags, main_logger.log(
exclude_tags, f"Skipping removal request {r.id} for {r.attributes.deployment} because it does"
r.attributes.tags)) f"not appear to refer to a live, named deployment."
)
elif skip_by_tag(r, include_tags, exclude_tags):
main_logger.log(
"Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)"
% (r.id, include_tags, exclude_tags, r.attributes.tags)
)
elif r.id in removals_by_request: elif r.id in removals_by_request:
print(f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}") main_logger.log(
f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}"
)
elif r.attributes.deployment in removals_by_deployment: elif r.attributes.deployment in removals_by_deployment:
print( main_logger.log(
f"Found removal record for indicated deployment {r.attributes.deployment} at " f"Found removal record for indicated deployment {r.attributes.deployment} at "
f"{removals_by_deployment[r.attributes.deployment].id}") f"{removals_by_deployment[r.attributes.deployment].id}"
)
else: else:
if r.id not in previous_requests: if r.id not in previous_requests:
print(f"Request {r.id} needs to processed.") main_logger.log(f"Request {r.id} needs to processed.")
requests_to_check_for_payment.append(r)
else:
main_logger.log(
f"Skipping unsatisfied request {r.id} because we have seen it before."
)
except Exception as e:
main_logger.log(f"ERROR examining {r.id}: {e}")
requests_to_execute = []
if min_required_payment:
for r in requests_to_check_for_payment:
main_logger.log(f"{r.id}: Confirming payment...")
if confirm_payment(
laconic,
r,
payment_address,
min_required_payment,
main_logger,
):
main_logger.log(f"{r.id}: Payment confirmed.")
requests_to_execute.append(r) requests_to_execute.append(r)
else: else:
print(f"Skipping unsatisfied request {r.id} because we have seen it before.") main_logger.log(f"Skipping request {r.id}: unable to verify payment.")
dump_known_requests(state_file, [r])
else:
requests_to_execute = requests_to_check_for_payment
print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) main_logger.log(
"Found %d unsatisfied request(s) to process." % len(requests_to_execute)
)
if not dry_run: if not dry_run:
for r in requests_to_execute: for r in requests_to_execute:
@ -202,7 +342,10 @@ def command(ctx, laconic_config, deployment_parent_dir,
r, r,
os.path.abspath(deployment_parent_dir), os.path.abspath(deployment_parent_dir),
delete_volumes, delete_volumes,
delete_names delete_names,
deployer_record,
) )
except Exception as e:
main_logger.log(f"ERROR processing removal request {r.id}: {e}")
finally: finally:
dump_known_requests(state_file, [r]) dump_known_requests(state_file, [r])

View File

@ -1,4 +1,4 @@
# Copyright © 2023 Vulcanize # = str(min_required_payment) Copyright © 2023 Vulcanize
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@ -22,7 +22,6 @@ import subprocess
import sys import sys
import tempfile import tempfile
import uuid import uuid
import yaml import yaml
@ -83,6 +82,84 @@ def match_owner(recordA, *records):
return None return None
def is_lrn(name_or_id: str):
if name_or_id:
return str(name_or_id).startswith("lrn://")
return False
def is_id(name_or_id: str):
return not is_lrn(name_or_id)
def confirm_payment(laconic, record, payment_address, min_amount, logger):
req_owner = laconic.get_owner(record)
if req_owner == payment_address:
# No need to confirm payment if the sender and recipient are the same account.
return True
if not record.attributes.payment:
logger.log(f"{record.id}: no payment tx info")
return False
tx = laconic.get_tx(record.attributes.payment)
if not tx:
logger.log(f"{record.id}: cannot locate payment tx")
return False
if tx.code != 0:
logger.log(
f"{record.id}: payment tx {tx.hash} was not successful - code: {tx.code}, log: {tx.log}"
)
return False
if tx.sender != req_owner:
logger.log(
f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment "
f"request owner {req_owner}"
)
return False
if tx.recipient != payment_address:
logger.log(
f"{record.id}: payment recipient {tx.recipient} in tx {tx.hash} does not match {payment_address}"
)
return False
pay_denom = "".join([i for i in tx.amount if not i.isdigit()])
if pay_denom != "alnt":
logger.log(
f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected payment denomination"
)
return False
pay_amount = int("".join([i for i in tx.amount if i.isdigit()]))
if pay_amount < min_amount:
logger.log(
f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}"
)
return False
# Check if the payment was already used on a
used = laconic.app_deployments(
{"deployer": payment_address, "payment": tx.hash}, all=True
)
if len(used):
logger.log(f"{record.id}: payment {tx.hash} already used on deployment {used}")
return False
used = laconic.app_deployment_removals(
{"deployer": payment_address, "payment": tx.hash}, all=True
)
if len(used):
logger.log(
f"{record.id}: payment {tx.hash} already used on deployment removal {used}"
)
return False
return True
class LaconicRegistryClient: class LaconicRegistryClient:
def __init__(self, config_file, log_file=None): def __init__(self, config_file, log_file=None):
self.config_file = config_file self.config_file = config_file
@ -90,11 +167,95 @@ class LaconicRegistryClient:
self.cache = AttrDict( self.cache = AttrDict(
{ {
"name_or_id": {}, "name_or_id": {},
"accounts": {},
"txs": {},
} }
) )
def list_records(self, criteria={}, all=False): def whoami(self, refresh=False):
args = ["laconic", "-c", self.config_file, "cns", "record", "list"] if not refresh and "whoami" in self.cache:
return self.cache["whoami"]
args = ["laconic", "-c", self.config_file, "registry", "account", "get"]
results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
if len(results):
self.cache["whoami"] = results[0]
return results[0]
return None
def get_owner(self, record, require=False):
bond = self.get_bond(record.bondId, require)
if bond:
return bond.owner
return bond
def get_account(self, address, refresh=False, require=False):
if not refresh and address in self.cache["accounts"]:
return self.cache["accounts"][address]
args = [
"laconic",
"-c",
self.config_file,
"registry",
"account",
"get",
"--address",
address,
]
results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
if len(results):
self.cache["accounts"][address] = results[0]
return results[0]
if require:
raise Exception("Cannot locate account:", address)
return None
def get_bond(self, id, require=False):
if id in self.cache.name_or_id:
return self.cache.name_or_id[id]
args = [
"laconic",
"-c",
self.config_file,
"registry",
"bond",
"get",
"--id",
id,
]
results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
self._add_to_cache(results)
if len(results):
return results[0]
if require:
raise Exception("Cannot locate bond:", id)
return None
def list_bonds(self):
args = ["laconic", "-c", self.config_file, "registry", "bond", "list"]
results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
self._add_to_cache(results)
return results
def list_records(self, criteria=None, all=False):
if criteria is None:
criteria = {}
args = ["laconic", "-c", self.config_file, "registry", "record", "list"]
if all: if all:
args.append("--all") args.append("--all")
@ -104,22 +265,17 @@ class LaconicRegistryClient:
args.append("--%s" % k) args.append("--%s" % k)
args.append(str(v)) args.append(str(v))
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
# Most recent records first # Most recent records first
results.sort(key=lambda r: r.createTime) results.sort(key=lambda r: r.createTime)
results.reverse() results.reverse()
self._add_to_cache(results)
return results return results
def is_crn(self, name_or_id: str):
if name_or_id:
return str(name_or_id).startswith("crn://")
return False
def is_id(self, name_or_id: str):
return not self.is_crn(name_or_id)
def _add_to_cache(self, records): def _add_to_cache(self, records):
if not records: if not records:
return return
@ -127,8 +283,9 @@ class LaconicRegistryClient:
for p in records: for p in records:
self.cache["name_or_id"][p.id] = p self.cache["name_or_id"][p.id] = p
if p.names: if p.names:
for crn in p.names: for lrn in p.names:
self.cache["name_or_id"][crn] = p self.cache["name_or_id"][lrn] = p
if p.attributes and p.attributes.type:
if p.attributes.type not in self.cache: if p.attributes.type not in self.cache:
self.cache[p.attributes.type] = [] self.cache[p.attributes.type] = []
self.cache[p.attributes.type].append(p) self.cache[p.attributes.type].append(p)
@ -140,9 +297,11 @@ class LaconicRegistryClient:
if name in self.cache.name_or_id: if name in self.cache.name_or_id:
return self.cache.name_or_id[name] return self.cache.name_or_id[name]
args = ["laconic", "-c", self.config_file, "cns", "name", "resolve", name] args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name]
parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] parsed = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
if parsed: if parsed:
self._add_to_cache(parsed) self._add_to_cache(parsed)
return parsed[0] return parsed[0]
@ -158,21 +317,23 @@ class LaconicRegistryClient:
if name_or_id in self.cache.name_or_id: if name_or_id in self.cache.name_or_id:
return self.cache.name_or_id[name_or_id] return self.cache.name_or_id[name_or_id]
if self.is_crn(name_or_id): if is_lrn(name_or_id):
return self.resolve(name_or_id) return self.resolve(name_or_id)
args = [ args = [
"laconic", "laconic",
"-c", "-c",
self.config_file, self.config_file,
"cns", "registry",
"record", "record",
"get", "get",
"--id", "--id",
name_or_id, name_or_id,
] ]
parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] parsed = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
if len(parsed): if len(parsed):
self._add_to_cache(parsed) self._add_to_cache(parsed)
return parsed[0] return parsed[0]
@ -181,29 +342,85 @@ class LaconicRegistryClient:
raise Exception("Cannot locate record:", name_or_id) raise Exception("Cannot locate record:", name_or_id)
return None return None
def app_deployment_requests(self, all=True): def get_tx(self, txHash, require=False):
return self.list_records({"type": "ApplicationDeploymentRequest"}, all) if txHash in self.cache["txs"]:
return self.cache["txs"][txHash]
def app_deployments(self, all=True): args = [
return self.list_records({"type": "ApplicationDeploymentRecord"}, all) "laconic",
"-c",
self.config_file,
"registry",
"tokens",
"gettx",
"--hash",
txHash,
]
def app_deployment_removal_requests(self, all=True): parsed = None
return self.list_records({"type": "ApplicationDeploymentRemovalRequest"}, all) try:
parsed = AttrDict(json.loads(logged_cmd(self.log_file, *args)))
except: # noqa: E722
pass
def app_deployment_removals(self, all=True): if parsed:
return self.list_records({"type": "ApplicationDeploymentRemovalRecord"}, all) self.cache["txs"][txHash] = parsed
return parsed
def publish(self, record, names=[]): if require:
raise Exception("Cannot locate tx:", hash)
def app_deployment_requests(self, criteria=None, all=True):
if criteria is None:
criteria = {}
criteria = criteria.copy()
criteria["type"] = "ApplicationDeploymentRequest"
return self.list_records(criteria, all)
def app_deployments(self, criteria=None, all=True):
if criteria is None:
criteria = {}
criteria = criteria.copy()
criteria["type"] = "ApplicationDeploymentRecord"
return self.list_records(criteria, all)
def app_deployment_removal_requests(self, criteria=None, all=True):
if criteria is None:
criteria = {}
criteria = criteria.copy()
criteria["type"] = "ApplicationDeploymentRemovalRequest"
return self.list_records(criteria, all)
def app_deployment_removals(self, criteria=None, all=True):
if criteria is None:
criteria = {}
criteria = criteria.copy()
criteria["type"] = "ApplicationDeploymentRemovalRecord"
return self.list_records(criteria, all)
def publish(self, record, names=None):
if names is None:
names = []
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
try: try:
record_fname = os.path.join(tmpdir, "record.yml") record_fname = os.path.join(tmpdir, "record.yml")
record_file = open(record_fname, 'w') record_file = open(record_fname, "w")
yaml.dump(record, record_file) yaml.dump(record, record_file)
record_file.close() record_file.close()
print(open(record_fname, 'r').read(), file=self.log_file) print(open(record_fname, "r").read(), file=self.log_file)
new_record_id = json.loads( new_record_id = json.loads(
logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "record", "publish", "--filename", record_fname) logged_cmd(
self.log_file,
"laconic",
"-c",
self.config_file,
"registry",
"record",
"publish",
"--filename",
record_fname,
)
)["id"] )["id"]
for name in names: for name in names:
self.set_name(name, new_record_id) self.set_name(name, new_record_id)
@ -212,10 +429,47 @@ class LaconicRegistryClient:
logged_cmd(self.log_file, "rm", "-rf", tmpdir) logged_cmd(self.log_file, "rm", "-rf", tmpdir)
def set_name(self, name, record_id): def set_name(self, name, record_id):
logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "name", "set", name, record_id) logged_cmd(
self.log_file,
"laconic",
"-c",
self.config_file,
"registry",
"name",
"set",
name,
record_id,
)
def delete_name(self, name): def delete_name(self, name):
logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "name", "delete", name) logged_cmd(
self.log_file,
"laconic",
"-c",
self.config_file,
"registry",
"name",
"delete",
name,
)
def send_tokens(self, address, amount, type="alnt"):
args = [
"laconic",
"-c",
self.config_file,
"registry",
"tokens",
"send",
"--address",
address,
"--quantity",
str(amount),
"--type",
type,
]
return AttrDict(json.loads(logged_cmd(self.log_file, *args)))
def file_hash(filename): def file_hash(filename):
@ -239,9 +493,12 @@ def determine_base_container(clone_dir, app_type="webapp"):
return base_container return base_container
def build_container_image(app_record, tag, extra_build_args=[], logger=None): def build_container_image(app_record, tag, extra_build_args=None, logger=None):
if extra_build_args is None:
extra_build_args = []
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
# TODO: determine if this code could be calling into the Python git library like setup-repositories
try: try:
record_id = app_record["id"] record_id = app_record["id"]
ref = app_record.attributes.repository_ref ref = app_record.attributes.repository_ref
@ -249,35 +506,72 @@ def build_container_image(app_record, tag, extra_build_args=[], logger=None):
clone_dir = os.path.join(tmpdir, record_id) clone_dir = os.path.join(tmpdir, record_id)
logger.log(f"Cloning repository {repo} to {clone_dir} ...") logger.log(f"Cloning repository {repo} to {clone_dir} ...")
# Set github credentials if present running a command like:
# git config --global url."https://${TOKEN}:@github.com/".insteadOf "https://github.com/"
github_token = os.environ.get("DEPLOYER_GITHUB_TOKEN")
if github_token:
logger.log("Github token detected, setting it in the git environment")
git_config_args = [
"git",
"config",
"--global",
f"url.https://{github_token}:@github.com/.insteadOf",
"https://github.com/",
]
result = subprocess.run(
git_config_args, stdout=logger.file, stderr=logger.file
)
result.check_returncode()
if ref: if ref:
# TODO: Determing branch or hash, and use depth 1 if we can. # TODO: Determing branch or hash, and use depth 1 if we can.
git_env = dict(os.environ.copy()) git_env = dict(os.environ.copy())
# Never prompt # Never prompt
git_env["GIT_TERMINAL_PROMPT"] = "0" git_env["GIT_TERMINAL_PROMPT"] = "0"
try: try:
subprocess.check_call(["git", "clone", repo, clone_dir], env=git_env, stdout=logger.file, stderr=logger.file) subprocess.check_call(
["git", "clone", repo, clone_dir],
env=git_env,
stdout=logger.file,
stderr=logger.file,
)
except Exception as e: except Exception as e:
logger.log(f"git clone failed. Is the repository {repo} private?") logger.log(f"git clone failed. Is the repository {repo} private?")
raise e raise e
try: try:
subprocess.check_call(["git", "checkout", ref], cwd=clone_dir, env=git_env, stdout=logger.file, stderr=logger.file) subprocess.check_call(
["git", "checkout", ref],
cwd=clone_dir,
env=git_env,
stdout=logger.file,
stderr=logger.file,
)
except Exception as e: except Exception as e:
logger.log(f"git checkout failed. Does ref {ref} exist?") logger.log(f"git checkout failed. Does ref {ref} exist?")
raise e raise e
else: else:
result = subprocess.run(["git", "clone", "--depth", "1", repo, clone_dir], stdout=logger.file, stderr=logger.file) # TODO: why is this code different vs the branch above (run vs check_call, and no prompt disable)?
result = subprocess.run(
["git", "clone", "--depth", "1", repo, clone_dir],
stdout=logger.file,
stderr=logger.file,
)
result.check_returncode() result.check_returncode()
base_container = determine_base_container(clone_dir, app_record.attributes.app_type) base_container = determine_base_container(
clone_dir, app_record.attributes.app_type
)
logger.log("Building webapp ...") logger.log("Building webapp ...")
build_command = [ build_command = [
sys.argv[0], sys.argv[0],
"--verbose", "--verbose",
"build-webapp", "build-webapp",
"--source-repo", clone_dir, "--source-repo",
"--tag", tag, clone_dir,
"--base-container", base_container "--tag",
tag,
"--base-container",
base_container,
] ]
if extra_build_args: if extra_build_args:
build_command.append("--extra-build-args") build_command.append("--extra-build-args")
@ -291,38 +585,57 @@ def build_container_image(app_record, tag, extra_build_args=[], logger=None):
def push_container_image(deployment_dir, logger): def push_container_image(deployment_dir, logger):
logger.log("Pushing images ...") logger.log("Pushing images ...")
result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"], result = subprocess.run(
stdout=logger.file, stderr=logger.file) [sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"],
stdout=logger.file,
stderr=logger.file,
)
result.check_returncode() result.check_returncode()
logger.log("Finished pushing images.") logger.log("Finished pushing images.")
def deploy_to_k8s(deploy_record, deployment_dir, logger): def deploy_to_k8s(deploy_record, deployment_dir, recreate, logger):
if not deploy_record:
command = "up"
else:
command = "update"
logger.log("Deploying to k8s ...") logger.log("Deploying to k8s ...")
result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, command],
stdout=logger.file, stderr=logger.file) if recreate:
commands_to_run = ["stop", "start"]
else:
if not deploy_record:
commands_to_run = ["start"]
else:
commands_to_run = ["update"]
for command in commands_to_run:
logger.log(f"Running {command} command on deployment dir: {deployment_dir}")
result = subprocess.run(
[sys.argv[0], "deployment", "--dir", deployment_dir, command],
stdout=logger.file,
stderr=logger.file,
)
result.check_returncode() result.check_returncode()
logger.log(f"Finished {command} command on deployment dir: {deployment_dir}")
logger.log("Finished deploying to k8s.") logger.log("Finished deploying to k8s.")
def publish_deployment(laconic: LaconicRegistryClient, def publish_deployment(
laconic: LaconicRegistryClient,
app_record, app_record,
deploy_record, deploy_record,
deployment_crn, deployment_lrn,
dns_record, dns_record,
dns_crn, dns_lrn,
deployment_dir, deployment_dir,
app_deployment_request=None, app_deployment_request=None,
logger=None): webapp_deployer_record=None,
logger=None,
):
if not deploy_record: if not deploy_record:
deploy_ver = "0.0.1" deploy_ver = "0.0.1"
else: else:
deploy_ver = "0.0.%d" % (int(deploy_record.attributes.version.split(".")[-1]) + 1) deploy_ver = "0.0.%d" % (
int(deploy_record.attributes.version.split(".")[-1]) + 1
)
if not dns_record: if not dns_record:
dns_ver = "0.0.1" dns_ver = "0.0.1"
@ -340,9 +653,7 @@ def publish_deployment(laconic: LaconicRegistryClient,
"version": dns_ver, "version": dns_ver,
"name": fqdn, "name": fqdn,
"resource_type": "A", "resource_type": "A",
"meta": { "meta": {"so": uniq.hex},
"so": uniq.hex
},
} }
} }
if app_deployment_request: if app_deployment_request:
@ -350,7 +661,7 @@ def publish_deployment(laconic: LaconicRegistryClient,
if logger: if logger:
logger.log("Publishing DnsRecord.") logger.log("Publishing DnsRecord.")
dns_id = laconic.publish(new_dns_record, [dns_crn]) dns_id = laconic.publish(new_dns_record, [dns_lrn])
new_deployment_record = { new_deployment_record = {
"record": { "record": {
@ -362,25 +673,34 @@ def publish_deployment(laconic: LaconicRegistryClient,
"dns": dns_id, "dns": dns_id,
"meta": { "meta": {
"config": file_hash(os.path.join(deployment_dir, "config.env")), "config": file_hash(os.path.join(deployment_dir, "config.env")),
"so": uniq.hex "so": uniq.hex,
}, },
} }
} }
if app_deployment_request: if app_deployment_request:
new_deployment_record["record"]["request"] = app_deployment_request.id new_deployment_record["record"]["request"] = app_deployment_request.id
if app_deployment_request.attributes.payment:
new_deployment_record["record"][
"payment"
] = app_deployment_request.attributes.payment
if webapp_deployer_record:
new_deployment_record["record"]["deployer"] = webapp_deployer_record.names[0]
if logger: if logger:
logger.log("Publishing ApplicationDeploymentRecord.") logger.log("Publishing ApplicationDeploymentRecord.")
deployment_id = laconic.publish(new_deployment_record, [deployment_crn]) deployment_id = laconic.publish(new_deployment_record, [deployment_lrn])
return {"dns": dns_id, "deployment": deployment_id} return {"dns": dns_id, "deployment": deployment_id}
def hostname_for_deployment_request(app_deployment_request, laconic): def hostname_for_deployment_request(app_deployment_request, laconic):
dns_name = app_deployment_request.attributes.dns dns_name = app_deployment_request.attributes.dns
if not dns_name: if not dns_name:
app = laconic.get_record(app_deployment_request.attributes.application, require=True) app = laconic.get_record(
app_deployment_request.attributes.application, require=True
)
dns_name = generate_hostname_for_app(app) dns_name = generate_hostname_for_app(app)
elif dns_name.startswith("crn://"): elif dns_name.startswith("lrn://"):
record = laconic.get_record(dns_name, require=True) record = laconic.get_record(dns_name, require=True)
dns_name = record.attributes.name dns_name = record.attributes.name
return dns_name return dns_name

View File

@ -24,7 +24,9 @@ from stack_orchestrator.build import build_webapp
from stack_orchestrator.deploy.webapp import (run_webapp, from stack_orchestrator.deploy.webapp import (run_webapp,
deploy_webapp, deploy_webapp,
deploy_webapp_from_registry, deploy_webapp_from_registry,
undeploy_webapp_from_registry) undeploy_webapp_from_registry,
publish_webapp_deployer,
request_webapp_deployment)
from stack_orchestrator.deploy import deploy from stack_orchestrator.deploy import deploy
from stack_orchestrator import version from stack_orchestrator import version
from stack_orchestrator.deploy import deployment from stack_orchestrator.deploy import deployment
@ -61,6 +63,8 @@ cli.add_command(run_webapp.command, "run-webapp")
cli.add_command(deploy_webapp.command, "deploy-webapp") cli.add_command(deploy_webapp.command, "deploy-webapp")
cli.add_command(deploy_webapp_from_registry.command, "deploy-webapp-from-registry") cli.add_command(deploy_webapp_from_registry.command, "deploy-webapp-from-registry")
cli.add_command(undeploy_webapp_from_registry.command, "undeploy-webapp-from-registry") cli.add_command(undeploy_webapp_from_registry.command, "undeploy-webapp-from-registry")
cli.add_command(publish_webapp_deployer.command, "publish-deployer-to-registry")
cli.add_command(request_webapp_deployment.command, "request-webapp-deployment")
cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-system cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-system
cli.add_command(deploy.command, "deploy-system") cli.add_command(deploy.command, "deploy-system")
cli.add_command(deployment.command, "deployment") cli.add_command(deployment.command, "deployment")

View File

@ -20,14 +20,12 @@ import os
import sys import sys
from decouple import config from decouple import config
import git import git
from git.exc import GitCommandError
from tqdm import tqdm from tqdm import tqdm
import click import click
import importlib.resources import importlib.resources
from pathlib import Path
import yaml
from stack_orchestrator.constants import stack_file_name
from stack_orchestrator.opts import opts from stack_orchestrator.opts import opts
from stack_orchestrator.util import include_exclude_check, stack_is_external, error_exit, warn_exit from stack_orchestrator.util import get_parsed_stack_config, include_exclude_check, error_exit, warn_exit
class GitProgress(git.RemoteProgress): class GitProgress(git.RemoteProgress):
@ -81,9 +79,13 @@ def _get_repo_current_branch_or_tag(full_filesystem_repo_path):
except TypeError: except TypeError:
# This means that the current ref is not a branch, so possibly a tag # This means that the current ref is not a branch, so possibly a tag
# Let's try to get the tag # Let's try to get the tag
try:
current_repo_branch_or_tag = git.Repo(full_filesystem_repo_path).git.describe("--tags", "--exact-match") current_repo_branch_or_tag = git.Repo(full_filesystem_repo_path).git.describe("--tags", "--exact-match")
# Note that git is assymetric -- the tag you told it to check out may not be the one # Note that git is asymmetric -- the tag you told it to check out may not be the one
# you get back here (if there are multiple tags associated with the same commit) # you get back here (if there are multiple tags associated with the same commit)
except GitCommandError:
# If there is no matching branch or tag checked out, just use the current SHA
current_repo_branch_or_tag = git.Repo(full_filesystem_repo_path).commit("HEAD").hexsha
return current_repo_branch_or_tag, is_branch return current_repo_branch_or_tag, is_branch
@ -102,7 +104,7 @@ def process_repo(pull, check_only, git_ssh, dev_root_path, branches_array, fully
full_filesystem_repo_path full_filesystem_repo_path
) if is_present else (None, None) ) if is_present else (None, None)
if not opts.o.quiet: if not opts.o.quiet:
present_text = f"already exists active {'branch' if is_branch else 'tag'}: {current_repo_branch_or_tag}" if is_present \ present_text = f"already exists active {'branch' if is_branch else 'ref'}: {current_repo_branch_or_tag}" if is_present \
else 'Needs to be fetched' else 'Needs to be fetched'
print(f"Checking: {full_filesystem_repo_path}: {present_text}") print(f"Checking: {full_filesystem_repo_path}: {present_text}")
# Quick check that it's actually a repo # Quick check that it's actually a repo
@ -120,7 +122,7 @@ def process_repo(pull, check_only, git_ssh, dev_root_path, branches_array, fully
origin = git_repo.remotes.origin origin = git_repo.remotes.origin
origin.pull(progress=None if opts.o.quiet else GitProgress()) origin.pull(progress=None if opts.o.quiet else GitProgress())
else: else:
print("skipping pull because this repo checked out a tag") print("skipping pull because this repo is not on a branch")
else: else:
print("(git pull skipped)") print("(git pull skipped)")
if not is_present: if not is_present:
@ -222,19 +224,9 @@ def command(ctx, include, exclude, git_ssh, check_only, pull, branches):
repos_in_scope = [] repos_in_scope = []
if stack: if stack:
if stack_is_external(stack): stack_config = get_parsed_stack_config(stack)
stack_file_path = Path(stack).joinpath(stack_file_name)
else:
# In order to be compatible with Python 3.8 we need to use this hack to get the path:
# See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure
stack_file_path = Path(__file__).absolute().parent.parent.joinpath("data", "stacks", stack, stack_file_name)
if not stack_file_path.exists():
error_exit(f"stack {stack} does not exist")
with stack_file_path:
stack_config = yaml.safe_load(open(stack_file_path, "r"))
if "repos" not in stack_config or stack_config["repos"] is None: if "repos" not in stack_config or stack_config["repos"] is None:
warn_exit(f"stack {stack} does not define any repositories") warn_exit(f"stack {stack} does not define any repositories")
else:
repos_in_scope = stack_config["repos"] repos_in_scope = stack_config["repos"]
else: else:
repos_in_scope = all_repos repos_in_scope = all_repos

View File

@ -20,6 +20,7 @@ import ruamel.yaml
from pathlib import Path from pathlib import Path
from dotenv import dotenv_values from dotenv import dotenv_values
from typing import Mapping, Set, List from typing import Mapping, Set, List
from stack_orchestrator.constants import stack_file_name, deployment_file_name
def include_exclude_check(s, include, exclude): def include_exclude_check(s, include, exclude):
@ -33,11 +34,14 @@ def include_exclude_check(s, include, exclude):
return s not in exclude_list return s not in exclude_list
def get_stack_file_path(stack): def get_stack_path(stack):
if stack_is_external(stack):
stack_path = Path(stack)
else:
# In order to be compatible with Python 3.8 we need to use this hack to get the path: # In order to be compatible with Python 3.8 we need to use this hack to get the path:
# See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure # See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure
stack_file_path = Path(__file__).absolute().parent.joinpath("data", "stacks", stack, "stack.yml") stack_path = Path(__file__).absolute().parent.joinpath("data", "stacks", stack)
return stack_file_path return stack_path
def get_dev_root_path(ctx): def get_dev_root_path(ctx):
@ -52,21 +56,14 @@ def get_dev_root_path(ctx):
# Caller can pass either the name of a stack, or a path to a stack file # Caller can pass either the name of a stack, or a path to a stack file
def get_parsed_stack_config(stack): def get_parsed_stack_config(stack):
stack_file_path = stack if isinstance(stack, os.PathLike) else get_stack_file_path(stack) stack_file_path = get_stack_path(stack).joinpath(stack_file_name)
try: if stack_file_path.exists():
with stack_file_path: return get_yaml().load(open(stack_file_path, "r"))
stack_config = get_yaml().load(open(stack_file_path, "r"))
return stack_config
except FileNotFoundError as error:
# We try here to generate a useful diagnostic error # We try here to generate a useful diagnostic error
# First check if the stack directory is present # First check if the stack directory is present
stack_directory = stack_file_path.parent if stack_file_path.parent.exists():
if os.path.exists(stack_directory): error_exit(f"stack.yml file is missing from stack: {stack}")
print(f"Error: stack.yml file is missing from stack: {stack}") error_exit(f"stack {stack} does not exist")
else:
print(f"Error: stack: {stack} does not exist")
print(f"Exiting, error: {error}")
sys.exit(1)
def get_pod_list(parsed_stack): def get_pod_list(parsed_stack):
@ -87,7 +84,7 @@ def get_plugin_code_paths(stack) -> List[Path]:
result: Set[Path] = set() result: Set[Path] = set()
for pod in pods: for pod in pods:
if type(pod) is str: if type(pod) is str:
result.add(get_stack_file_path(stack).parent) result.add(get_stack_path(stack))
else: else:
pod_root_dir = os.path.join(get_dev_root_path(None), pod["repository"].split("/")[-1], pod["path"]) pod_root_dir = os.path.join(get_dev_root_path(None), pod["repository"].split("/")[-1], pod["path"])
result.add(Path(os.path.join(pod_root_dir, "stack"))) result.add(Path(os.path.join(pod_root_dir, "stack")))
@ -199,6 +196,13 @@ def stack_is_external(stack: str):
return Path(stack).exists() if stack is not None else False return Path(stack).exists() if stack is not None else False
def stack_is_in_deployment(stack: Path):
if isinstance(stack, os.PathLike):
return stack.joinpath(deployment_file_name).exists()
else:
return False
def get_yaml(): def get_yaml():
# See: https://stackoverflow.com/a/45701840/1701505 # See: https://stackoverflow.com/a/45701840/1701505
yaml = ruamel.yaml.YAML() yaml = ruamel.yaml.YAML()

View File

@ -14,7 +14,7 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>. # along with this program. If not, see <http:#www.gnu.org/licenses/>.
import click import click
import importlib.resources from importlib import resources, metadata
@click.command() @click.command()
@ -24,8 +24,11 @@ def command(ctx):
# See: https://stackoverflow.com/a/20885799/1701505 # See: https://stackoverflow.com/a/20885799/1701505
from stack_orchestrator import data from stack_orchestrator import data
with importlib.resources.open_text(data, "build_tag.txt") as version_file: if resources.is_resource(data, "build_tag.txt"):
with resources.open_text(data, "build_tag.txt") as version_file:
# TODO: code better version that skips comment lines # TODO: code better version that skips comment lines
version_string = version_file.read().splitlines()[1] version_string = version_file.read().splitlines()[1]
else:
version_string = metadata.version("laconic-stack-orchestrator") + "-unknown"
print(f"Version: {version_string}") print(version_string)

View File

@ -1,44 +0,0 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
set -e
echo "Running stack-orchestrator Ethereum plugeth fixturenet test"
# Bit of a hack, test the most recent package
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
CERC_STACK_NAME=fixturenet-plugeth-tx
# Set a new unique repo dir
export CERC_REPO_BASE_DIR=$(mktemp -d stack-orchestrator-fixturenet-eth-test.XXXXXXXXXX)
echo "Testing this package: $TEST_TARGET_SO"
echo "Test version command"
reported_version_string=$( $TEST_TARGET_SO version )
echo "Version reported is: ${reported_version_string}"
echo "Cloning repositories into: $CERC_REPO_BASE_DIR"
$TEST_TARGET_SO --stack $CERC_STACK_NAME setup-repositories
echo "Building containers"
$TEST_TARGET_SO --stack $CERC_STACK_NAME build-containers
echo "Images in registry:"
docker image ls
echo "Deploying the cluster"
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy up
# Verify that the fixturenet is up and running
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy ps
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec fixturenet-eth-bootnode-lighthouse /scripts/status-internal.sh
initial_block_number=$($TEST_TARGET_SO --stack fixturenet-plugeth-tx deploy exec foundry "cast block-number")
# Check that the block number increases some time later
sleep 12
subsequent_block_number=$($TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec foundry "cast block-number")
block_number_difference=$((subsequent_block_number - initial_block_number))
# Block height difference should be between 1 and some small number
if [[ $block_number_difference -gt 1 && $block_number_difference -lt 10 ]]; then
echo "Test passed"
test_result=0
else
echo "Test failed: block numbers were ${initial_block_number} and ${subsequent_block_number}"
test_result=1
fi
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy down
echo "Removing cloned repositories"
rm -rf $CERC_REPO_BASE_DIR
exit $test_result

View File

@ -1,80 +0,0 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
echo "$(date +"%Y-%m-%d %T"): Running stack-orchestrator Ethereum fixturenet test"
# Bit of a hack, test the most recent package
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
# Set a new unique repo dir
export CERC_REPO_BASE_DIR=$(mktemp -d stack-orchestrator-fixturenet-eth-test.XXXXXXXXXX)
echo "$(date +"%Y-%m-%d %T"): Testing this package: $TEST_TARGET_SO"
echo "$(date +"%Y-%m-%d %T"): Test version command"
reported_version_string=$( $TEST_TARGET_SO version )
echo "$(date +"%Y-%m-%d %T"): Version reported is: ${reported_version_string}"
echo "$(date +"%Y-%m-%d %T"): Cloning repositories into: $CERC_REPO_BASE_DIR"
$TEST_TARGET_SO --stack fixturenet-eth setup-repositories
echo "$(date +"%Y-%m-%d %T"): Building containers"
$TEST_TARGET_SO --stack fixturenet-eth build-containers
echo "$(date +"%Y-%m-%d %T"): Starting stack"
$TEST_TARGET_SO --stack fixturenet-eth deploy up
echo "$(date +"%Y-%m-%d %T"): Stack started"
# Verify that the fixturenet is up and running
$TEST_TARGET_SO --stack fixturenet-eth deploy ps
# echo "$(date +"%Y-%m-%d %T"): Getting stack status"
# $TEST_TARGET_SO --stack fixturenet-eth deploy exec fixturenet-eth-bootnode-lighthouse /scripts/status-internal.sh
timeout=900 # 15 minutes
echo "$(date +"%Y-%m-%d %T"): Getting initial block number. Timeout set to $timeout seconds"
start_time=$(date +%s)
elapsed_time=0
initial_block_number=0
while [ "$initial_block_number" -eq 0 ] && [ $elapsed_time -lt $timeout ]; do
sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for initial block..."
initial_block_number=$($TEST_TARGET_SO --stack fixturenet-eth deploy exec foundry "cast block-number")
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
done
subsequent_block_number=$initial_block_number
# if initial block was 0 after timeout, assume chain did not start successfully and skip finding subsequent block
if [[ $initial_block_number -gt 0 ]]; then
timeout=300
echo "$(date +"%Y-%m-%d %T"): Getting subsequent block number. Timeout set to $timeout seconds"
start_time=$(date +%s)
elapsed_time=0
# wait for 5 blocks or timeout
while [ "$subsequent_block_number" -le $((initial_block_number + 5)) ] && [ $elapsed_time -lt $timeout ]; do
sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for five blocks or $timeout seconds..."
subsequent_block_number=$($TEST_TARGET_SO --stack fixturenet-eth deploy exec foundry "cast block-number")
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
done
fi
# will return 0 if either of the above loops timed out
block_number_difference=$((subsequent_block_number - initial_block_number))
echo "$(date +"%Y-%m-%d %T"): Results of block height queries:"
echo "Initial block height: $initial_block_number"
echo "Subsequent block height: $subsequent_block_number"
# Block height difference should be between 1 and some small number
if [[ $block_number_difference -gt 1 && $block_number_difference -lt 100 ]]; then
echo "Test passed"
test_result=0
else
echo "Test failed: block numbers were ${initial_block_number} and ${subsequent_block_number}"
echo "Logs from stack:"
$TEST_TARGET_SO --stack fixturenet-eth deploy logs
test_result=1
fi
$TEST_TARGET_SO --stack fixturenet-eth deploy down --delete-volumes
echo "$(date +"%Y-%m-%d %T"): Removing cloned repositories"
rm -rf $CERC_REPO_BASE_DIR
echo "$(date +"%Y-%m-%d %T"): Test finished"
exit $test_result

View File

@ -37,7 +37,7 @@ initial_block_number=null
while [ "$initial_block_number" == "null" ] && [ $elapsed_time -lt $timeout ]; do while [ "$initial_block_number" == "null" ] && [ $elapsed_time -lt $timeout ]; do
sleep 10 sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for initial block..." echo "$(date +"%Y-%m-%d %T"): Waiting for initial block..."
initial_block_number=$(docker exec laconicd-laconicd-1 /usr/bin/laconicd status | jq -r .SyncInfo.latest_block_height) initial_block_number=$(docker exec laconicd-laconicd-1 /usr/bin/laconicd status | jq -r .sync_info.latest_block_height)
current_time=$(date +%s) current_time=$(date +%s)
elapsed_time=$((current_time - start_time)) elapsed_time=$((current_time - start_time))
done done
@ -54,7 +54,7 @@ if [[ $initial_block_number != "null" ]]; then
while [ "$subsequent_block_number" -le $((initial_block_number + 5)) ] && [ $elapsed_time -lt $timeout ]; do while [ "$subsequent_block_number" -le $((initial_block_number + 5)) ] && [ $elapsed_time -lt $timeout ]; do
sleep 10 sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for five blocks or $timeout seconds..." echo "$(date +"%Y-%m-%d %T"): Waiting for five blocks or $timeout seconds..."
subsequent_block_number=$(docker exec laconicd-laconicd-1 /usr/bin/laconicd status | jq -r .SyncInfo.latest_block_height) subsequent_block_number=$(docker exec laconicd-laconicd-1 /usr/bin/laconicd status | jq -r .sync_info.latest_block_height)
current_time=$(date +%s) current_time=$(date +%s)
elapsed_time=$((current_time - start_time)) elapsed_time=$((current_time - start_time))
done done

View File

@ -0,0 +1,222 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
# Dump environment variables for debugging
echo "Environment variables:"
env
fi
if [ "$1" == "from-path" ]; then
TEST_TARGET_SO="laconic-so"
else
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
fi
# Helper functions: TODO move into a separate file
wait_for_pods_started () {
for i in {1..50}
do
local ps_output=$( $TEST_TARGET_SO deployment --dir $test_deployment_dir ps )
if [[ "$ps_output" == *"Running containers:"* ]]; then
# if ready, return
return
else
# if not ready, wait
sleep 5
fi
done
# Timed out, error exit
echo "waiting for pods to start: FAILED"
delete_cluster_exit
}
wait_for_log_output () {
for i in {1..50}
do
local log_output=$( $TEST_TARGET_SO deployment --dir $test_deployment_dir logs )
if [[ ! -z "$log_output" ]]; then
# if ready, return
return
else
# if not ready, wait
sleep 5
fi
done
# Timed out, error exit
echo "waiting for pods log content: FAILED"
delete_cluster_exit
}
delete_cluster_exit () {
$TEST_TARGET_SO deployment --dir $test_deployment_dir stop --delete-volumes
exit 1
}
# Set a non-default repo dir
export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir
echo "Testing this package: $TEST_TARGET_SO"
echo "Test version command"
reported_version_string=$( $TEST_TARGET_SO version )
echo "Version reported is: ${reported_version_string}"
echo "Cloning repositories into: $CERC_REPO_BASE_DIR"
rm -rf $CERC_REPO_BASE_DIR
mkdir -p $CERC_REPO_BASE_DIR
$TEST_TARGET_SO --stack test setup-repositories
$TEST_TARGET_SO --stack test build-containers
# Test basic stack-orchestrator deploy to k8s
test_deployment_dir=$CERC_REPO_BASE_DIR/test-deployment-dir
test_deployment_spec=$CERC_REPO_BASE_DIR/test-deployment-spec.yml
# Create a deployment that we can use to check our test cases
$TEST_TARGET_SO --stack test deploy --deploy-to k8s-kind init --output $test_deployment_spec
# Check the file now exists
if [ ! -f "$test_deployment_spec" ]; then
echo "deploy init test: spec file not present"
echo "deploy init test: FAILED"
exit 1
fi
echo "deploy init test: passed"
$TEST_TARGET_SO --stack test deploy create --spec-file $test_deployment_spec --deployment-dir $test_deployment_dir
# Check the deployment dir exists
if [ ! -d "$test_deployment_dir" ]; then
echo "deploy create test: deployment directory not present"
echo "deploy create test: FAILED"
exit 1
fi
echo "deploy create test: passed"
# Check the file writted by the create command in the stack now exists
if [ ! -f "$test_deployment_dir/create-file" ]; then
echo "deploy create test: create output file not present"
echo "deploy create test: FAILED"
exit 1
fi
echo "deploy create output file test: passed"
# At this point the deployment's kind-config.yml will look like this:
# kind: Cluster
# apiVersion: kind.x-k8s.io/v1alpha4
# nodes:
# - role: control-plane
# kubeadmConfigPatches:
# - |
# kind: InitConfiguration
# nodeRegistration:
# kubeletExtraArgs:
# node-labels: "ingress-ready=true"
# extraPortMappings:
# - containerPort: 80
# hostPort: 80
# We need to change it to this:
# Note we also turn up the log level on the scheduler in order to diagnose placement errors
# See logs like: kubectl -n kube-system logs kube-scheduler-laconic-f185cd245d8dba98-control-plane
kind_config_file=${test_deployment_dir}/kind-config.yml
cat << EOF > ${kind_config_file}
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
scheduler:
extraArgs:
v: "3"
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
- role: worker
labels:
nodetype: a
- role: worker
labels:
nodetype: b
- role: worker
labels:
nodetype: c
kubeadmConfigPatches:
- |
kind: JoinConfiguration
nodeRegistration:
taints:
- key: "nodeavoid"
value: "c"
effect: "NoSchedule"
EOF
# At this point we should have 4 nodes, three labeled like this:
# $ kubectl get nodes --show-labels=true
# NAME STATUS ROLES AGE VERSION LABELS
# laconic-3af549a3ba0e3a3c-control-plane Ready control-plane 2m37s v1.30.0 ...,ingress-ready=true
# laconic-3af549a3ba0e3a3c-worker Ready <none> 2m18s v1.30.0 ...,nodetype=a
# laconic-3af549a3ba0e3a3c-worker2 Ready <none> 2m18s v1.30.0 ...,nodetype=b
# laconic-3af549a3ba0e3a3c-worker3 Ready <none> 2m18s v1.30.0 ...,nodetype=c
# And with taints like this:
# $ kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints --no-headers
# laconic-3af549a3ba0e3a3c-control-plane [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
# laconic-3af549a3ba0e3a3c-worker <none>
# laconic-3af549a3ba0e3a3c-worker2 <none>
# laconic-3af549a3ba0e3a3c-worker3 [map[effect:NoSchedule key:nodeavoid value:c]]
# We can now modify the deployment spec file to require a set of affinity and/or taint combinations
# then bring up the deployment and check that the pod is scheduled to an expected node.
# Add a requirement to schedule on a node labeled nodetype=c and
# a toleration such that no other pods schedule on that node
deployment_spec_file=${test_deployment_dir}/spec.yml
cat << EOF >> ${deployment_spec_file}
node-affinities:
- label: nodetype
value: c
node-tolerations:
- key: nodeavoid
value: c
EOF
# Get the deployment ID so we can generate low level kubectl commands later
deployment_id=$(cat ${test_deployment_dir}/deployment.yml | cut -d ' ' -f 2)
# Try to start the deployment
$TEST_TARGET_SO deployment --dir $test_deployment_dir start
wait_for_pods_started
# Check logs command works
wait_for_log_output
sleep 1
log_output_1=$( $TEST_TARGET_SO deployment --dir $test_deployment_dir logs )
if [[ "$log_output_1" == *"filesystem is fresh"* ]]; then
echo "deployment of pod test: passed"
else
echo "deployment pod test: FAILED"
echo $log_output_1
delete_cluster_exit
fi
# The deployment's pod should be scheduled onto node: worker3
# Check that's what happened
# Get get the node onto which the stack pod has been deployed
deployment_node=$(kubectl get pods -l app=${deployment_id} -o=jsonpath='{.items..spec.nodeName}')
expected_node=${deployment_id}-worker3
echo "Stack pod deployed to node: ${deployment_node}"
if [[ ${deployment_node} == ${expected_node} ]]; then
echo "deployment of pod test: passed"
else
echo "deployment pod test: FAILED"
echo "Stack pod deployed to node: ${deployment_node}, expected node: ${expected_node}"
delete_cluster_exit
fi
# Stop and clean up
$TEST_TARGET_SO deployment --dir $test_deployment_dir stop --delete-volumes
echo "Test passed"

View File

@ -4,6 +4,10 @@ if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x set -x
fi fi
set_ownership () {
sudo chown $USER: -R $1
}
node_count=4 node_count=4
node_dir_prefix="laconic-network-dir" node_dir_prefix="laconic-network-dir"
chain_id="laconic_81337-6" chain_id="laconic_81337-6"
@ -15,7 +19,21 @@ do
node_network_dir=${node_dir_prefix}${i} node_network_dir=${node_dir_prefix}${i}
if [[ -d $node_network_dir ]]; then if [[ -d $node_network_dir ]]; then
echo "Deleting ${node_network_dir}" echo "Deleting ${node_network_dir}"
rm -rf ${node_network_dir} sudo rm -rf ${node_network_dir}
fi
done
echo "Deleting any existing deployments..."
for (( i=1 ; i<=$node_count ; i++ ));
do
node_deployment_dir=${node_dir_prefix}${i}-deployment
node_spec_file=${node_dir_prefix}${i}-spec.yml
if [[ -d $node_deployment_dir ]]; then
echo "Deleting ${node_deployment_dir}"
rm -rf ${node_deployment_dir}
fi
if [[ -f $node_spec_file ]]; then
echo "Deleting ${node_spec_file}"
rm ${node_spec_file}
fi fi
done done
@ -25,6 +43,7 @@ do
node_network_dir=${node_dir_prefix}${i} node_network_dir=${node_dir_prefix}${i}
node_moniker=${node_moniker_prefix}${i} node_moniker=${node_moniker_prefix}${i}
laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --initialize-network --chain-id ${chain_id} --node-moniker ${node_moniker} laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --initialize-network --chain-id ${chain_id} --node-moniker ${node_moniker}
set_ownership ${node_network_dir}
done done
echo "Joining ${node_count} nodes to the network..." echo "Joining ${node_count} nodes to the network..."
@ -33,6 +52,7 @@ do
node_network_dir=${node_dir_prefix}${i} node_network_dir=${node_dir_prefix}${i}
node_moniker=${node_moniker_prefix}${i} node_moniker=${node_moniker_prefix}${i}
laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --join-network --key-name ${node_moniker} laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --join-network --key-name ${node_moniker}
set_ownership ${node_network_dir}
done done
echo "Merging ${node_count} nodes genesis txns..." echo "Merging ${node_count} nodes genesis txns..."
@ -43,11 +63,15 @@ for (( i=2 ; i<=$node_count ; i++ ));
do do
node_network_dir=${node_dir_prefix}${i} node_network_dir=${node_dir_prefix}${i}
node_gentx_file=$(ls ${node_network_dir}/config/gentx/*.json) node_gentx_file=$(ls ${node_network_dir}/config/gentx/*.json)
node_gentx_address=$(grep address ${node_network_dir}/config/genesis.json | head -1 | cut -d '"' -f 4)
gentx_files+=${delimeter}${node_gentx_file} gentx_files+=${delimeter}${node_gentx_file}
gentx_addresses+=${delimeter}${node_gentx_address}
delimeter="," delimeter=","
done done
echo "gentx files:"
echo ${gentx_files}
# Generate the genesis file on node 1 # Generate the genesis file on node 1
laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_dir_prefix}1 --create-network --gentx-files ${gentx_files} laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_dir_prefix}1 --create-network --gentx-files ${gentx_files} --gentx-addresses ${gentx_addresses}
genesis_file=${node_dir_prefix}1/config/genesis.json genesis_file=${node_dir_prefix}1/config/genesis.json
# Now import the genesis file to the other nodes # Now import the genesis file to the other nodes
for (( i=2 ; i<=$node_count ; i++ )); for (( i=2 ; i<=$node_count ; i++ ));
@ -55,4 +79,14 @@ do
echo "Importing genesis.json into node ${i}" echo "Importing genesis.json into node ${i}"
node_network_dir=${node_dir_prefix}${i} node_network_dir=${node_dir_prefix}${i}
laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --create-network --genesis-file ${genesis_file} laconic-so --stack mainnet-laconic deploy setup --network-dir ${node_network_dir} --create-network --genesis-file ${genesis_file}
set_ownership ${node_network_dir}
done
# Create deployments
echo "Creating ${node_count} deployments..."
for (( i=1 ; i<=$node_count ; i++ ));
do
node_network_dir=${node_dir_prefix}${i}
laconic-so --stack mainnet-laconic deploy init --output ${node_network_dir}-spec.yml
laconic-so --stack mainnet-laconic deploy create --deployment-dir ${node_network_dir}-deployment --spec-file ${node_dir_prefix}${i}-spec.yml --network-dir ${node_network_dir}
done done

View File

@ -20,7 +20,7 @@ echo "Cloning repositories into: $CERC_REPO_BASE_DIR"
rm -rf $CERC_REPO_BASE_DIR rm -rf $CERC_REPO_BASE_DIR
mkdir -p $CERC_REPO_BASE_DIR mkdir -p $CERC_REPO_BASE_DIR
# Pull an example small public repo to test we can pull a repo # Pull an example small public repo to test we can pull a repo
$TEST_TARGET_SO setup-repositories --include cerc-io/laconic-sdk $TEST_TARGET_SO setup-repositories --include cerc-io/registry-sdk
# Test pulling a stack # Test pulling a stack
$TEST_TARGET_SO --stack test setup-repositories $TEST_TARGET_SO --stack test setup-repositories
# Test building the a stack container # Test building the a stack container

View File

@ -10,8 +10,11 @@ echo "Environment variables:"
env env
# Test basic stack-orchestrator webapp # Test basic stack-orchestrator webapp
echo "Running stack-orchestrator webapp test" echo "Running stack-orchestrator webapp test"
# Bit of a hack, test the most recent package if [ "$1" == "from-path" ]; then
TEST_TARGET_SO="laconic-so"
else
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 ) TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
fi
# Set a non-default repo dir # Set a non-default repo dir
export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir
echo "Testing this package: $TEST_TARGET_SO" echo "Testing this package: $TEST_TARGET_SO"
@ -30,14 +33,17 @@ CHECK="SPECIAL_01234567890_TEST_STRING"
set +e set +e
CONTAINER_ID=$(docker run -p 3000:80 -d -e CERC_SCRIPT_DEBUG=$CERC_SCRIPT_DEBUG cerc/test-progressive-web-app:local) app_image_name="cerc/test-progressive-web-app:local"
CONTAINER_ID=$(docker run -p 3000:80 -d -e CERC_SCRIPT_DEBUG=$CERC_SCRIPT_DEBUG ${app_image_name})
sleep 3 sleep 3
wget --tries 20 --retry-connrefused --waitretry=3 -O test.before -m http://localhost:3000 wget --tries 20 --retry-connrefused --waitretry=3 -O test.before -m http://localhost:3000
docker logs $CONTAINER_ID docker logs $CONTAINER_ID
docker remove -f $CONTAINER_ID docker remove -f $CONTAINER_ID
CONTAINER_ID=$(docker run -p 3000:80 -e CERC_WEBAPP_DEBUG=$CHECK -e CERC_SCRIPT_DEBUG=$CERC_SCRIPT_DEBUG -d cerc/test-progressive-web-app:local) echo "Running app container test"
CONTAINER_ID=$(docker run -p 3000:80 -e CERC_WEBAPP_DEBUG=$CHECK -e CERC_SCRIPT_DEBUG=$CERC_SCRIPT_DEBUG -d ${app_image_name})
sleep 3 sleep 3
wget --tries 20 --retry-connrefused --waitretry=3 -O test.after -m http://localhost:3000 wget --tries 20 --retry-connrefused --waitretry=3 -O test.after -m http://localhost:3000
@ -63,4 +69,18 @@ else
echo "AFTER: PASSED" echo "AFTER: PASSED"
fi fi
echo "Running deployment create test"
# Note: this is not a full test -- all we're testing here is that the deploy-webapp create command doesn't crash
test_deployment_dir=$CERC_REPO_BASE_DIR/test-deployment-dir
fake_k8s_config_file=$CERC_REPO_BASE_DIR/kube-config.yml
touch ${fake_k8s_config_file}
$TEST_TARGET_SO deploy-webapp create --kube-config ${fake_k8s_config_file} --deployment-dir ${test_deployment_dir} --image ${app_image_name} --url https://my-test-app.example.com
if [ -d ${test_deployment_dir} ]; then
echo "PASSED"
else
echo "FAILED"
exit 1
fi
exit 0 exit 0