mirror of
https://github.com/railwayapp/cli
synced 2026-04-21 14:07:23 +00:00
Migrate to CLI v3 (#304)
* delete everything * migrate to v3 * basic logout command * remove pre-release text * additional error handling in rlwy domain * fixed backwards logic on rlwy domain * accept environment id in rlwy run * default environment on rlwy link if there's only 1 * dont print plugins and services if there are none * skip serializing nones * remove entities file * rename ServiceDomains * link to project upon init * remove description prompt * fully remove description prompt * remove dead code * refactor some of the commands * update repo target * fix broken render_config * cleanup unused vars * remove muts * bump cargo version to 3.0.0 * change repo on Cargo.toml --------- Co-authored-by: Angelo <asara019@fiu.edu>
This commit is contained in:
parent
e3bd751b88
commit
0cfb79da46
177 changed files with 9196 additions and 8223 deletions
11
.github/changelog-configuration.json
vendored
Normal file
11
.github/changelog-configuration.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"categories": [
|
||||
{
|
||||
"title": "## Changes",
|
||||
"labels": [],
|
||||
"exhaustive": false
|
||||
}
|
||||
],
|
||||
"template": "${{CHANGELOG}}\n",
|
||||
"pr_template": "- #${{NUMBER}} ${{TITLE}}"
|
||||
}
|
||||
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
|
|
@ -1,43 +0,0 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
# REMOVE WHEN RESOLVED
|
||||
# 1) https://github.com/golangci/golangci-lint-action/issues/135
|
||||
# 2) https://github.com/golangci/golangci-lint-action/issues/81
|
||||
- name: Clean modcache
|
||||
run: go clean -modcache
|
||||
|
||||
- name: Lint CLI
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
88
.github/workflows/ci.yml
vendored
Normal file
88
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: CI
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
lints:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
# Set linting rules for clippy
|
||||
args: --all-targets
|
||||
|
||||
test-plan:
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
matrix: ${{ steps.docker-prep.outputs.matrix }}
|
||||
if: "!contains(github.event.head_commit.message, '(cargo-release)')"
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
4
.github/workflows/label-check.yml
vendored
4
.github/workflows/label-check.yml
vendored
|
|
@ -11,6 +11,6 @@ jobs:
|
|||
- uses: jesusvasquez333/verify-pr-label-action@v1.4.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
valid-labels: "release/patch, release/minor, release/major"
|
||||
pull-request-number: '${{ github.event.pull_request.number }}'
|
||||
valid-labels: "release/patch, release/minor, release/major, release/skip"
|
||||
pull-request-number: "${{ github.event.pull_request.number }}"
|
||||
disable-reviews: true
|
||||
|
|
|
|||
90
.github/workflows/publish.yml
vendored
90
.github/workflows/publish.yml
vendored
|
|
@ -1,90 +0,0 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [publish-event]
|
||||
|
||||
jobs:
|
||||
release_and_brew:
|
||||
name: Release and bump Brew
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
id: go
|
||||
with:
|
||||
go-version: ^1.13
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Bump Homebrew Core
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
formula: railway
|
||||
tag: ${{ github.event.client_payload.new-tag }}
|
||||
revision: ${{ github.event.client_payload.sha }}
|
||||
|
||||
publish_npm:
|
||||
name: Publish to NPM
|
||||
needs: release_and_brew
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set version
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Check version
|
||||
run: echo "Version ${{ github.event.client_payload.new-tag }}"
|
||||
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Setup Git user
|
||||
run: |
|
||||
git config --global user.name "Github Bot"
|
||||
git config --global user.email "github-bot@railway.app"
|
||||
|
||||
- name: Create .npmrc file
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Bump NPM version
|
||||
run: npm --no-git-tag-version --allow-same-version version ${{ github.event.client_payload.new-tag }}
|
||||
|
||||
- name: NPM publish
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Discord Deployment Status Notification
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DEPLOY_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: "Published CLI"
|
||||
description: "Published CLI version ${{ github.event.client_payload.new-tag }} to Brew and NPM"
|
||||
nofail: false
|
||||
nodetail: false
|
||||
username: Github Actions
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
154
.github/workflows/release.yml
vendored
Normal file
154
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.7
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
rlwy_version: ${{ env.CLI_VERSION }}
|
||||
|
||||
steps:
|
||||
- name: Get the release version from the tag
|
||||
shell: bash
|
||||
if: env.CLI_VERSION == ''
|
||||
run: |
|
||||
# Apparently, this is the right way to get a tag name. Really?
|
||||
#
|
||||
# See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
echo "CLI_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "version is: ${{ env.CLI_VERSION }}"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Build Changelog
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v3.7.0
|
||||
with:
|
||||
configuration: ".github/changelog-configuration.json"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub release
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.CLI_VERSION }}
|
||||
release_name: ${{ env.CLI_VERSION }}
|
||||
|
||||
build-release:
|
||||
name: Build Release Assets
|
||||
needs: ["create-release"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
|
||||
- target: i686-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
|
||||
- target: arm-unknown-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
|
||||
- target: x86_64-apple-darwin
|
||||
os: macOS-latest
|
||||
|
||||
- target: aarch64-apple-darwin
|
||||
os: macOS-latest
|
||||
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
|
||||
- target: i686-pc-windows-msvc
|
||||
os: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Build release binary
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --locked --target ${{ matrix.target }}
|
||||
use-cross: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
|
||||
- name: Prepare binaries [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip rlwy.exe
|
||||
7z a ../../../rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.zip rlwy.exe
|
||||
cd -
|
||||
|
||||
- name: Prepare binaries [-linux]
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip rlwy || true
|
||||
tar czvf ../../../rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.tar.gz rlwy
|
||||
cd -
|
||||
|
||||
- name: Upload release archive
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ needs.create-release.outputs.rlwy_version }}
|
||||
files: rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}*
|
||||
|
||||
- name: Install cargo-deb
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: cargo install cargo-deb
|
||||
|
||||
- name: Generate .deb package file
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: cargo deb --target x86_64-unknown-linux-musl --output rlwy-${{ needs.create-release.outputs.rlwy_version }}-amd64.deb
|
||||
|
||||
- name: Upload .deb package file
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
tag: ${{ needs.create-release.outputs.rlwy_version }}
|
||||
file: rlwy-${{ needs.create-release.outputs.rlwy_version }}-amd64.deb
|
||||
|
||||
- name: Update homebrew tap
|
||||
uses: mislav/bump-homebrew-formula-action@v2
|
||||
if: "matrix.target == 'x86_64-apple-darwin' || matrix.target == 'aarch64-apple-darwin' && !contains(github.ref, '-')"
|
||||
with:
|
||||
formula-name: rlwy
|
||||
formula-path: rlwy.rb
|
||||
homebrew-tap: railwayapp/homebrew-tap
|
||||
download-url: https://github.com/railwayapp/cli/releases/latest/download/rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.tar.gz
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
||||
58
.github/workflows/tag.yml
vendored
58
.github/workflows/tag.yml
vendored
|
|
@ -1,58 +0,0 @@
|
|||
name: Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-ecosystem/action-get-merged-pull-request@v1
|
||||
id: get-merged-pull-request
|
||||
with:
|
||||
github_token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- uses: actions-ecosystem/action-release-label@v1
|
||||
id: release-label
|
||||
if: ${{ steps.get-merged-pull-request.outputs.title != null }}
|
||||
with:
|
||||
github_token: ${{ secrets.GH_PAT }}
|
||||
labels: ${{ steps.get-merged-pull-request.outputs.labels }}
|
||||
|
||||
- uses: actions-ecosystem/action-get-latest-tag@v1
|
||||
id: get-latest-tag
|
||||
if: ${{ steps.release-label.outputs.level != null }}
|
||||
with:
|
||||
semver_only: true
|
||||
|
||||
- uses: actions-ecosystem/action-bump-semver@v1
|
||||
id: bump-semver
|
||||
if: ${{ steps.release-label.outputs.level != null }}
|
||||
with:
|
||||
current_version: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
level: ${{ steps.release-label.outputs.level }}
|
||||
|
||||
- uses: actions-ecosystem/action-regex-match@v2
|
||||
id: regex-match
|
||||
if: ${{ steps.bump-semver.outputs.new_version != null }}
|
||||
with:
|
||||
text: ${{ steps.get-merged-pull-request.outputs.body }}
|
||||
regex: '```release_note([\s\S]*)```'
|
||||
|
||||
- uses: actions-ecosystem/action-push-tag@v1
|
||||
if: ${{ steps.bump-semver.outputs.new_version != null }}
|
||||
with:
|
||||
tag: ${{ steps.bump-semver.outputs.new_version }}
|
||||
message: "${{ steps.bump-semver.outputs.new_version }}: PR #${{ steps.get-merged-pull-request.outputs.number }} ${{ steps.get-merged-pull-request.outputs.title }}"
|
||||
|
||||
- uses: peter-evans/repository-dispatch@v1
|
||||
if: ${{ steps.bump-semver.outputs.new_version != null }}
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
repository: railwayapp/cli
|
||||
event-type: publish-event
|
||||
client-payload: '{"new-tag": "${{ steps.bump-semver.outputs.new_version }}", "release-notes": "${{ steps.regex-match.outputs.group1 }}"}'
|
||||
148
.gitignore
vendored
148
.gitignore
vendored
|
|
@ -1,147 +1 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/go,node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go,node
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,node
|
||||
|
||||
# Build directory
|
||||
bin/*
|
||||
!bin/*.js
|
||||
|
||||
# NPM token
|
||||
.npmrc
|
||||
|
||||
# Railway directories
|
||||
.railway
|
||||
|
||||
# bin
|
||||
cli
|
||||
/target
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# Documentation at http://goreleaser.com
|
||||
project_name: railway
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- binary: railway
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X github.com/railwayapp/cli/constants.Version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
brews:
|
||||
- tap:
|
||||
owner: railwayapp
|
||||
name: homebrew-railway
|
||||
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@railway.app
|
||||
|
||||
homepage: "https://railway.app"
|
||||
description: "Develop and deploy code with zero configuration"
|
||||
|
||||
install: |
|
||||
bin.install "railway"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}"
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
# Contribute to the Railway CLI
|
||||
|
||||
## Setup
|
||||
## Prerequisites
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [CMake](https://cmake.org/install/)
|
||||
|
||||
Run
|
||||
OR
|
||||
|
||||
```shell
|
||||
make run
|
||||
```
|
||||
- [Nix](https://nixos.org/download.html)
|
||||
|
||||
Build
|
||||
|
||||
```shell
|
||||
make build
|
||||
```
|
||||
### Nix Setup
|
||||
`nix-shell` to enter the shell with all the dependencies
|
||||
|
||||
### Running and Building
|
||||
`cargo run -- <args>` to run the binary\
|
||||
`cargo build --release` to build the binary
|
||||
2519
Cargo.lock
generated
Normal file
2519
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
75
Cargo.toml
Normal file
75
Cargo.toml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
[package]
|
||||
name = "railwayapp"
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Railway <contact@railway.app>"]
|
||||
description = "Interact with Railway via CLI"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/railwayapp/cli"
|
||||
repository = "https://github.com/railwayapp/cli"
|
||||
rust-version = "1.67.1"
|
||||
default-run = "railway"
|
||||
|
||||
[[bin]]
|
||||
name = "railway"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rlwy"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.69"
|
||||
clap = { version = "4.1.6", features = ["derive", "suggestions"] }
|
||||
colored = "2.0.0"
|
||||
dirs = "4.0.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.93"
|
||||
reqwest = { version = "0.11.14", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
] }
|
||||
chrono = { version = "0.4.23", features = ["serde"], default-features = false }
|
||||
graphql_client = { version = "0.11.0", features = ["reqwest-rustls"] }
|
||||
paste = "1.0.11"
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
clap_complete = "4.1.3"
|
||||
open = "3.2.0"
|
||||
inquire = "0.5.3"
|
||||
tui = "0.19.0"
|
||||
crossterm = "0.26.0"
|
||||
hyper = { version = "1.0.0-rc.3", features = ["server", "http1"] }
|
||||
base64 = "0.21.0"
|
||||
http-body-util = "0.1.0-rc.2"
|
||||
rand = "0.8.5"
|
||||
hostname = "0.3.1"
|
||||
indicatif = "0.17.3"
|
||||
indoc = "2.0.0"
|
||||
console = "0.15.5"
|
||||
box_drawing = "0.1.2"
|
||||
textwrap = "0.16.0"
|
||||
gzp = { version = "0.11.3", default-features = false, features = [
|
||||
"deflate_rust",
|
||||
] }
|
||||
tar = "0.4.38"
|
||||
synchronized-writer = "1.1.11"
|
||||
ignore = "0.4.20"
|
||||
num_cpus = "1.15.0"
|
||||
url = "2.3.1"
|
||||
futures = { version = "0.3.26", default-features = false, features = [
|
||||
"compat",
|
||||
"io-compat",
|
||||
] }
|
||||
tokio-stream = { version = "0.1.12", default-features = false, features = [
|
||||
"sync",
|
||||
] }
|
||||
uuid = { version = "1.3.0", features = ["serde", "v4"] }
|
||||
httparse = "1.8.0"
|
||||
names = { version = "0.14.0", default-features = false }
|
||||
graphql-ws-client = { version = "0.3.0", features = ["client-graphql-client"] }
|
||||
async-tungstenite = { version = "0.18.0", features = [
|
||||
"tokio-runtime",
|
||||
"tokio-rustls-native-certs",
|
||||
] }
|
||||
is-terminal = "0.4.4"
|
||||
serde_with = "2.2.0"
|
||||
4
LICENSE
4
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Railway Corp.
|
||||
Copyright (c) 2023 Railway Corp.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
5
Makefile
5
Makefile
|
|
@ -1,5 +0,0 @@
|
|||
build:
|
||||
@go build -o bin/railway
|
||||
|
||||
run:
|
||||
@go run main.go
|
||||
60
README.md
60
README.md
|
|
@ -1,57 +1,33 @@
|
|||
# Railway CLI
|
||||
# Railway CLI (3.x.x)
|
||||
|
||||

|
||||
[](https://github.com/railwayapp/cliv3/actions/workflows/ci.yml)
|
||||
|
||||
This is the command line interface for [Railway](https://railway.app). Use it to connect your code to Railways infrastructure without needing to worry about environment variables or configuration.
|
||||
This is the command line interface for [Railway](https://railway.app). Use it to connect your code to Railway's infrastructure without needing to worry about environment variables or configuration.
|
||||
|
||||
[View the docs](https://docs.railway.app/develop/cli)
|
||||
|
||||
The Railway command line interface (CLI) connects your code to your Railway project from the command line.
|
||||
|
||||
The Railway CLI allows you to
|
||||
|
||||
- Create new Railway projects from the terminal
|
||||
- Link to an existing Railway project
|
||||
- Pull down environment variables for your project locally to run
|
||||
- Create services and databases right from the comfort of your fingertips
|
||||
|
||||
## Installation
|
||||
|
||||
The Railway CLI is available through [Homebrew](https://brew.sh/), [NPM](https://www.npmjs.com/package/@railway/cli), curl, or as a [Nixpkg](https://nixos.org).
|
||||
|
||||
### Brew
|
||||
|
||||
```shell
|
||||
brew install railway
|
||||
### Cargo
|
||||
```bash
|
||||
cargo install railwayapp --locked
|
||||
```
|
||||
|
||||
### NPM
|
||||
|
||||
```shell
|
||||
npm i -g @railway/cli
|
||||
```
|
||||
|
||||
### Yarn
|
||||
|
||||
```shell
|
||||
yarn global add @railway/cli
|
||||
```
|
||||
|
||||
### curl
|
||||
|
||||
```shell
|
||||
curl -fsSL https://railway.app/install.sh | sh
|
||||
```
|
||||
|
||||
### Nixpkg
|
||||
Note: This installation method is not supported by Railway and is maintained by the community.
|
||||
```shell
|
||||
# On NixOS
|
||||
nix-env -iA nixos.railway
|
||||
# On non-NixOS
|
||||
nix-env -iA nixpkgs.railway
|
||||
```
|
||||
|
||||
### From source
|
||||
See [CONTRIBUTING.md](https://github.com/railwayapp/cli/blob/master/CONTRIBUTING.md) for information on setting up this repo locally.
|
||||
See [CONTRIBUTING.md](https://github.com/railwayapp/cliv3/blob/master/CONTRIBUTING.md) for information on setting up this repo locally.
|
||||
|
||||
## Documentation
|
||||
|
||||
[View the full documentation](https://docs.railway.app)
|
||||
|
||||
## Feedback
|
||||
|
||||
We would love to hear your feedback or suggestions. The best way to reach us is on [Discord](https://discord.gg/xAm2w6g).
|
||||
We would love to hear your feedback or suggestions. The best way to reach us is on [Discord](https://discord.gg/railway).
|
||||
|
||||
We also welcome pull requests into this repo. See [CONTRIBUTING.md](https://github.com/railwayapp/cli/blob/master/CONTRIBUTING.md) for information on setting up this repo locally.
|
||||
We also welcome pull requests into this repo. See [CONTRIBUTING.md](https://github.com/railwayapp/cliv3/blob/master/CONTRIBUTING.md) for information on setting up this repo locally.
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { execFileSync } from "child_process";
|
||||
import path from "path";
|
||||
import { exit } from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
try {
|
||||
execFileSync(path.resolve(`${__dirname}/railway`), process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch (e) {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
39
cmd/add.go
39
cmd/add.go
|
|
@ -1,39 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Add(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectCfg, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plugins, err := h.ctrl.GetAvailablePlugins(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedPlugin, err := ui.PromptPlugins(plugins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: fmt.Sprintf("Adding %s plugin", selectedPlugin),
|
||||
})
|
||||
defer ui.StopSpinner("")
|
||||
|
||||
plugin, err := h.ctrl.CreatePlugin(ctx, &entity.CreatePluginRequest{
|
||||
ProjectID: projectCfg.Project,
|
||||
Plugin: selectedPlugin,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("🎉 Created plugin %s\n", ui.MagentaText(plugin.Name))
|
||||
return nil
|
||||
|
||||
}
|
||||
24
cmd/build.go
24
cmd/build.go
|
|
@ -1,24 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Build(ctx context.Context, req *entity.CommandRequest) error {
|
||||
if h.cfg.RailwayProductionToken == "" {
|
||||
fmt.Println("Railway env file is only generated in production")
|
||||
return nil
|
||||
}
|
||||
|
||||
err := h.ctrl.SaveEnvsToFile(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(`Env written to %s
|
||||
Do NOT commit the env.json file. This command should only be run as a production build step.\n`, h.cfg.RailwayEnvFilePath)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Completion(ctx context.Context, req *entity.CommandRequest) error {
|
||||
switch req.Args[0] {
|
||||
case "bash":
|
||||
err := req.Cmd.Root().GenBashCompletion(os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "zsh":
|
||||
err := req.Cmd.Root().GenZshCompletion(os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "fish":
|
||||
err := req.Cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "powershell":
|
||||
err := req.Cmd.Root().GenPowerShellCompletion(os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
162
cmd/connect.go
162
cmd/connect.go
|
|
@ -1,162 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Connect(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectCfg, _ := h.ctrl.GetProjectConfigs(ctx)
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("🎉 Connecting to: %s %s\n", ui.MagentaText(project.Name), ui.MagentaText(environment.Name))
|
||||
|
||||
var plugin string
|
||||
|
||||
if len(req.Args) == 0 {
|
||||
names := make([]string, 0)
|
||||
for _, plugin := range project.Plugins {
|
||||
// TODO: Better way of handling this
|
||||
if plugin.Name != "env" {
|
||||
names = append(names, plugin.Name)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Select a database to connect to:")
|
||||
plugin, err = ui.PromptPlugins(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
plugin = req.Args[0]
|
||||
}
|
||||
|
||||
if !isPluginValid(plugin) {
|
||||
return fmt.Errorf("Invalid plugin: %s", plugin)
|
||||
}
|
||||
envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command, connectEnv := buildConnectCommand(plugin, envs)
|
||||
if !commandExistsInPath(command[0]) {
|
||||
fmt.Println("🚨", ui.RedText(command[0]), "was not found in $PATH.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range connectEnv {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%+v", k, v))
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
catchSignals(ctx, cmd, nil)
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandExistsInPath(cmd string) bool {
|
||||
// The error can be safely ignored because it indicates a failure to find the
|
||||
// command in $PATH.
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func isPluginValid(plugin string) bool {
|
||||
switch plugin {
|
||||
case "redis":
|
||||
fallthrough
|
||||
case "psql":
|
||||
fallthrough
|
||||
case "postgres":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
fallthrough
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "mongo":
|
||||
fallthrough
|
||||
case "mongodb":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildConnectCommand(plugin string, envs *entity.Envs) ([]string, map[string]string) {
|
||||
var command []string
|
||||
var connectEnv map[string]string
|
||||
|
||||
switch plugin {
|
||||
case "redis":
|
||||
// run
|
||||
command = []string{"redis-cli", "-u", (*envs)["REDIS_URL"]}
|
||||
case "psql":
|
||||
fallthrough
|
||||
case "postgres":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
connectEnv = map[string]string{
|
||||
"PGPASSWORD": (*envs)["PGPASSWORD"],
|
||||
}
|
||||
command = []string{
|
||||
"psql",
|
||||
"-U",
|
||||
(*envs)["PGUSER"],
|
||||
"-h",
|
||||
(*envs)["PGHOST"],
|
||||
"-p",
|
||||
(*envs)["PGPORT"],
|
||||
"-d",
|
||||
(*envs)["PGDATABASE"],
|
||||
}
|
||||
case "mongo":
|
||||
fallthrough
|
||||
case "mongodb":
|
||||
command = []string{
|
||||
"mongo",
|
||||
fmt.Sprintf(
|
||||
"mongodb://%s:%s@%s:%s",
|
||||
(*envs)["MONGOUSER"],
|
||||
(*envs)["MONGOPASSWORD"],
|
||||
(*envs)["MONGOHOST"],
|
||||
(*envs)["MONGOPORT"],
|
||||
),
|
||||
}
|
||||
case "mysql":
|
||||
command = []string{
|
||||
"mysql",
|
||||
fmt.Sprintf("-h%s", (*envs)["MYSQLHOST"]),
|
||||
fmt.Sprintf("-u%s", (*envs)["MYSQLUSER"]),
|
||||
fmt.Sprintf("-p%s", (*envs)["MYSQLPASSWORD"]),
|
||||
fmt.Sprintf("--port=%s", (*envs)["MYSQLPORT"]),
|
||||
"--protocol=TCP",
|
||||
(*envs)["MYSQLDATABASE"],
|
||||
}
|
||||
}
|
||||
return command, connectEnv
|
||||
}
|
||||
113
cmd/delete.go
113
cmd/delete.go
|
|
@ -1,113 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
"github.com/railwayapp/cli/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) Delete(ctx context.Context, req *entity.CommandRequest) error {
|
||||
user, err := h.ctrl.GetUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Has2FA {
|
||||
fmt.Printf("Your account has 2FA enabled, you must delete your project on the Dashboard.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(req.Args) > 0 {
|
||||
// projectID provided as argument
|
||||
arg := req.Args[0]
|
||||
|
||||
if uuid.IsValidUUID(arg) {
|
||||
project, err := h.ctrl.GetProject(ctx, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.ctrl.DeleteProject(ctx, project.Id)
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProjectByName(ctx, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.ctrl.DeleteProject(ctx, project.Id)
|
||||
}
|
||||
|
||||
isLoggedIn, err := h.ctrl.IsLoggedIn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isLoggedIn {
|
||||
return h.deleteFromAccount(ctx, req)
|
||||
}
|
||||
|
||||
return h.deleteFromID(ctx, req)
|
||||
}
|
||||
|
||||
func (h *Handler) deleteFromAccount(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projects, err := h.ctrl.GetProjects(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
fmt.Printf("No Projects found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := ui.PromptProjects(projects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ui.PromptConfirmProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if project.Name != name {
|
||||
fmt.Printf("The project name typed doesn't match the selected project to be deleted.")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("🔥 Deleting project %s\n", ui.MagentaText(name))
|
||||
err = h.deleteById(ctx, project.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✅ Deleted project %s\n", ui.MagentaText(name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) deleteFromID(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectID, err := ui.PromptText("Enter your project id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("🔥 Deleting project %s\n", ui.MagentaText(project.Name))
|
||||
err = h.deleteById(ctx, project.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✅ Deleted project %s\n", ui.MagentaText(project.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) deleteById(ctx context.Context, projectId string) error {
|
||||
err := h.ctrl.DeleteProject(ctx, projectId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Design(ctx context.Context, req *entity.CommandRequest) error {
|
||||
fmt.Print(ui.Heading("Alerts"))
|
||||
fmt.Print(ui.AlertDanger("Something bad is going to happen!"))
|
||||
fmt.Print(ui.AlertWarning("That might not have been what you wanted"))
|
||||
fmt.Print(ui.AlertInfo("Just so you know you know, Railway is awesome"))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Unordered List"))
|
||||
fmt.Print(ui.UnorderedList([]string{
|
||||
"List Item 1",
|
||||
"List Item 2",
|
||||
"List Item 3",
|
||||
}))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Ordered List"))
|
||||
fmt.Print(ui.OrderedList([]string{
|
||||
"First Step",
|
||||
"Next Step",
|
||||
}))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Key-Value Pairs"))
|
||||
fmt.Print(ui.KeyValues(map[string]string{
|
||||
"First Key": "Value 1",
|
||||
"Second Key": "Value 2",
|
||||
"Third Key": "Value 3",
|
||||
}))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Truncated Text"))
|
||||
fmt.Println(ui.Truncate("012345678901234567890123456789", 50))
|
||||
fmt.Println(ui.Truncate("012345678901234567890123456789", 10))
|
||||
fmt.Println(ui.Truncate("012345678901234567890123456789", 0))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Secret Text"))
|
||||
fmt.Printf("My super secret password is %s, the name of my childhood pet.\n", ui.ObscureText("Luke"))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Paragraph"))
|
||||
fmt.Print(ui.Paragraph("Paragraphs print the given text, but wrap it automatically when the lines are too long. It's super convenient!"))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Indented"))
|
||||
fmt.Print(ui.Indent("func main() {\n println(\"Hello World!\")\n}"))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Block Quote"))
|
||||
fmt.Print(ui.BlockQuote("That's the thing about counter-intuitive ideas. They contradict your intuitions. So, they seem wrong"))
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Print(ui.Heading("Generic Line Prefix"))
|
||||
fmt.Print(ui.PrefixLines("Line 1\nLine 2\nLine 3", "🥳 "))
|
||||
|
||||
return nil
|
||||
}
|
||||
12
cmd/docs.go
12
cmd/docs.go
|
|
@ -1,12 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/constants"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Docs(ctx context.Context, req *entity.CommandRequest) error {
|
||||
return h.ctrl.ConfirmBrowserOpen("Opening Railway Docs...", constants.RailwayDocsURL)
|
||||
}
|
||||
68
cmd/down.go
68
cmd/down.go
|
|
@ -1,68 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Down(ctx context.Context, req *entity.CommandRequest) error {
|
||||
isVerbose, err := req.Cmd.Flags().GetBool("verbose")
|
||||
|
||||
if err != nil {
|
||||
// Verbose mode isn't a necessary flag; just default to false.
|
||||
isVerbose = false
|
||||
}
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Using verbose mode"))
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading project configuration"))
|
||||
projectConfig, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading environment"))
|
||||
environmentName, err := req.Cmd.Flags().GetString("environment")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.getEnvironment(ctx, environmentName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Using environment %s", ui.Bold(environment.Name))))
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading project"))
|
||||
project, err := h.ctrl.GetProject(ctx, projectConfig.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bypass, err := req.Cmd.Flags().GetBool("yes")
|
||||
if err != nil {
|
||||
bypass = false
|
||||
}
|
||||
if !bypass {
|
||||
shouldDelete, err := ui.PromptYesNo(fmt.Sprintf("Delete latest deployment for project %s?", project.Name))
|
||||
if err != nil || !shouldDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = h.ctrl.Down(ctx, &entity.DownRequest{
|
||||
ProjectID: project.Id,
|
||||
EnvironmentID: environment.Id,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(ui.AlertInfo(fmt.Sprintf("Deleted latest deployment for project %s.", project.Name)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Environment(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectID, err := h.cfg.GetProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var environment *entity.Environment
|
||||
if len(req.Args) > 0 {
|
||||
var name = req.Args[0]
|
||||
|
||||
// Look for existing environment with name
|
||||
for _, projectEnvironment := range project.Environments {
|
||||
if name == projectEnvironment.Name {
|
||||
environment = projectEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
if (environment != nil) {
|
||||
fmt.Printf("%s Environment: %s\n", promptui.IconGood, ui.BlueText(environment.Name))
|
||||
} else {
|
||||
// Create new environment
|
||||
environment, err = h.ctrl.CreateEnvironment(ctx, &entity.CreateEnvironmentRequest{
|
||||
Name: name,
|
||||
ProjectID: project.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created Environment %s\nEnvironment: %s\n", promptui.IconGood, ui.BlueText(ui.Bold(name).String()))
|
||||
}
|
||||
} else {
|
||||
// Existing environment selector
|
||||
environment, err = ui.PromptEnvironments(project.Environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = h.cfg.SetEnvironment(environment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s ProTip: You can view the active environment by running %s\n", promptui.IconInitial, ui.BlueText("railway status"))
|
||||
return err
|
||||
}
|
||||
250
cmd/init.go
250
cmd/init.go
|
|
@ -1,250 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
CLIErrors "github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) initNew(ctx context.Context, req *entity.CommandRequest) error {
|
||||
name, err := ui.PromptProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.CreateProject(ctx, &entity.CreateProjectRequest{
|
||||
Name: &name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.cfg.SetNewProject(project.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := ui.PromptEnvironments(project.Environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.cfg.SetEnvironment(environment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if a .env exists, if so prompt uploading it
|
||||
err = h.ctrl.AutoImportDotEnv(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("🎉 Created project %s\n", ui.MagentaText(name))
|
||||
|
||||
return h.ctrl.OpenProjectInBrowser(ctx, project.Id, environment.Id)
|
||||
}
|
||||
|
||||
func (h *Handler) initFromTemplate(ctx context.Context, req *entity.CommandRequest) error {
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Fetching starter templates",
|
||||
})
|
||||
|
||||
starters, err := h.ctrl.GetStarters(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.StopSpinner("")
|
||||
|
||||
template, err := ui.PromptStarterTemplates(starters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse to get query params
|
||||
parsedUrl, err := url.ParseQuery(template.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
optionalEnvVars := parsedUrl.Get("optionalEnvs")
|
||||
envVars := strings.Split(parsedUrl.Get("envs"), ",")
|
||||
plugins := strings.Split(parsedUrl.Get("plugins"), ",")
|
||||
|
||||
// Prepare environment variables for prompt
|
||||
starterEnvVars := make([]*entity.StarterEnvVar, 0)
|
||||
for _, variable := range envVars {
|
||||
if variable != "" {
|
||||
var envVar = new(entity.StarterEnvVar)
|
||||
envVar.Name = variable
|
||||
envVar.Desc = parsedUrl.Get(variable + "Desc")
|
||||
envVar.Default = parsedUrl.Get(variable + "Default")
|
||||
envVar.Optional = strings.Contains(optionalEnvVars, variable)
|
||||
|
||||
starterEnvVars = append(starterEnvVars, envVar)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare plugins for creation
|
||||
starterPlugins := make([]string, 0)
|
||||
for _, plugin := range plugins {
|
||||
if plugin != "" {
|
||||
starterPlugins = append(starterPlugins, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
// Select GitHub owner
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Fetching GitHub scopes",
|
||||
})
|
||||
scopes, err := h.ctrl.GetWritableGithubScopes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(scopes) == 0 {
|
||||
return CLIErrors.NoGitHubScopesFound
|
||||
}
|
||||
ui.StopSpinner("")
|
||||
|
||||
owner, err := ui.PromptGitHubScopes(scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Enter project name
|
||||
name, err := ui.PromptProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isPrivate, err := ui.PromptIsRepoPrivate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prompt for env vars (if required)
|
||||
variables, err := ui.PromptEnvVars(starterEnvVars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Railway project
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Creating project",
|
||||
})
|
||||
creationResult, err := h.ctrl.CreateProjectFromTemplate(ctx, &entity.CreateProjectFromTemplateRequest{
|
||||
Name: name,
|
||||
Owner: owner,
|
||||
Template: template.Source,
|
||||
IsPrivate: isPrivate,
|
||||
Plugins: starterPlugins,
|
||||
Variables: variables,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, creationResult.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.StopSpinner("")
|
||||
|
||||
// Wait for workflow to complete
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Deploying project",
|
||||
})
|
||||
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
workflowStatus, err := h.ctrl.GetWorkflowStatus(ctx, creationResult.WorkflowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if workflowStatus.IsError() {
|
||||
ui.StopSpinner("Uhh Ohh. Workflow failed!")
|
||||
return CLIErrors.WorkflowFailed
|
||||
}
|
||||
if workflowStatus.IsComplete() {
|
||||
ui.StopSpinner("Project creation complete 🚀")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Select environment to activate
|
||||
environment, err := ui.PromptEnvironments(project.Environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.cfg.SetEnvironment(environment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if a .env exists, if so prompt uploading it
|
||||
err = h.ctrl.AutoImportDotEnv(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("🎉 Created project %s\n", ui.MagentaText(name))
|
||||
return h.ctrl.OpenProjectDeploymentsInBrowser(ctx, project.Id)
|
||||
}
|
||||
|
||||
func (h *Handler) setProject(ctx context.Context, project *entity.Project) error {
|
||||
err := h.cfg.SetNewProject(project.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := ui.PromptEnvironments(project.Environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.cfg.SetEnvironment(environment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) Init(ctx context.Context, req *entity.CommandRequest) error {
|
||||
if len(req.Args) > 0 {
|
||||
// NOTE: This is to support legacy `railway init <PROJECT_ID>` which should
|
||||
// now be `railway link <PROJECT_ID>`
|
||||
return h.Link(ctx, req)
|
||||
}
|
||||
|
||||
// Since init can be called by guests, ensure we can fetch a user first before calling. This prevents
|
||||
// us accidentally creating a temporary (guest) project if we have a token locally but our remote
|
||||
// session was deleted.
|
||||
_, err := h.ctrl.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s\nRun %s", ui.RedText("Account required to init project"), ui.Bold("railway login"))
|
||||
}
|
||||
|
||||
selection, err := ui.PromptInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch selection {
|
||||
case ui.InitNew:
|
||||
return h.initNew(ctx, req)
|
||||
case ui.InitFromTemplate:
|
||||
return h.initFromTemplate(ctx, req)
|
||||
default:
|
||||
return errors.New("Invalid selection")
|
||||
}
|
||||
}
|
||||
79
cmd/link.go
79
cmd/link.go
|
|
@ -1,79 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
"github.com/railwayapp/cli/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) Link(ctx context.Context, req *entity.CommandRequest) error {
|
||||
if len(req.Args) > 0 {
|
||||
// projectID provided as argument
|
||||
arg := req.Args[0]
|
||||
|
||||
if uuid.IsValidUUID(arg) {
|
||||
project, err := h.ctrl.GetProject(ctx, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.setProject(ctx, project)
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProjectByName(ctx, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.setProject(ctx, project)
|
||||
}
|
||||
|
||||
isLoggedIn, err := h.ctrl.IsLoggedIn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isLoggedIn {
|
||||
return h.linkFromAccount(ctx, req)
|
||||
} else {
|
||||
return h.linkFromID(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) linkFromAccount(ctx context.Context, _ *entity.CommandRequest) error {
|
||||
projects, err := h.ctrl.GetProjects(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
fmt.Print(ui.AlertWarning("No projects found"))
|
||||
fmt.Printf("Create one with %s\n", ui.GreenText("railway init"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
project, err := ui.PromptProjects(projects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.setProject(ctx, project)
|
||||
}
|
||||
|
||||
func (h *Handler) linkFromID(ctx context.Context, _ *entity.CommandRequest) error {
|
||||
projectID, err := ui.PromptText("Enter your project id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.setProject(ctx, project)
|
||||
}
|
||||
30
cmd/list.go
30
cmd/list.go
|
|
@ -1,30 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) List(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectId, err := h.cfg.GetProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projects, err := h.ctrl.GetProjects(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range projects {
|
||||
if projectId == v.Id {
|
||||
fmt.Println(ui.MagentaText(v.Name))
|
||||
continue
|
||||
}
|
||||
fmt.Println(ui.GrayText(v.Name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
cmd/login.go
25
cmd/login.go
|
|
@ -1,25 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Login(ctx context.Context, req *entity.CommandRequest) error {
|
||||
isBrowserless, err := req.Cmd.Flags().GetBool("browserless")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := h.ctrl.Login(ctx, isBrowserless)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\n🎉 Logged in as %s (%s)\n", ui.Bold(user.Name), user.Email)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Logout(ctx context.Context, req *entity.CommandRequest) error {
|
||||
return h.ctrl.Logout(ctx)
|
||||
}
|
||||
15
cmd/logs.go
15
cmd/logs.go
|
|
@ -1,15 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Logs(ctx context.Context, req *entity.CommandRequest) error {
|
||||
numLines, err := req.Cmd.Flags().GetInt32("lines")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.ctrl.GetActiveDeploymentLogs(ctx, numLines)
|
||||
}
|
||||
18
cmd/main.go
18
cmd/main.go
|
|
@ -1,18 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/railwayapp/cli/configs"
|
||||
"github.com/railwayapp/cli/controller"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
ctrl *controller.Controller
|
||||
cfg *configs.Configs
|
||||
}
|
||||
|
||||
func New() *Handler {
|
||||
return &Handler{
|
||||
ctrl: controller.New(),
|
||||
cfg: configs.New(),
|
||||
}
|
||||
}
|
||||
47
cmd/open.go
47
cmd/open.go
|
|
@ -1,47 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Open(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectId, err := h.cfg.GetProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
environmentId, err := h.cfg.GetCurrentEnvironment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If an unknown subcommand is used, show help
|
||||
if len(req.Args) > 0 {
|
||||
return req.Cmd.Help()
|
||||
}
|
||||
|
||||
if req.Cmd.Use == "open" {
|
||||
return h.ctrl.OpenProjectInBrowser(ctx, projectId, environmentId)
|
||||
}
|
||||
|
||||
return h.ctrl.OpenProjectPathInBrowser(ctx, projectId, environmentId, req.Cmd.Use)
|
||||
}
|
||||
|
||||
func (h *Handler) OpenApp(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectId, err := h.cfg.GetProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
environmentId, err := h.cfg.GetCurrentEnvironment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployment, err := h.ctrl.GetLatestDeploymentForEnvironment(ctx, projectId, environmentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.ctrl.OpenStaticUrlInBrowser(deployment.StaticUrl)
|
||||
}
|
||||
30
cmd/panic.go
30
cmd/panic.go
|
|
@ -1,30 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Panic(ctx context.Context, panicErr string, stacktrace string, cmd string, args []string) error {
|
||||
cmd = cmd + " " + strings.Join(args, " ")
|
||||
for _, arg := range args {
|
||||
if arg == "-v" {
|
||||
// Verbose mode show err
|
||||
fmt.Println(panicErr, stacktrace)
|
||||
}
|
||||
}
|
||||
|
||||
success, err := h.ctrl.SendPanic(ctx, panicErr, stacktrace, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if success {
|
||||
ui.StopSpinner("Successfully sent the error! We're figuring out what went wrong.")
|
||||
return nil
|
||||
}
|
||||
return errors.TelemetryFailed
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Protect(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectConfigs, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mp := make(map[string]bool)
|
||||
|
||||
for k, v := range projectConfigs.LockedEnvsNames {
|
||||
mp[k] = v
|
||||
}
|
||||
|
||||
mp[projectConfigs.Environment] = true
|
||||
|
||||
projectConfigs.LockedEnvsNames = mp
|
||||
|
||||
err = h.cfg.SetProjectConfigs(projectConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
301
cmd/run.go
301
cmd/run.go
|
|
@ -1,301 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
goErr "errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
var RAIL_PORT = 4411
|
||||
|
||||
func (h *Handler) getEnvironment(ctx context.Context, environmentName string) (*entity.Environment, error) {
|
||||
if environmentName == "" {
|
||||
return h.ctrl.GetCurrentEnvironment(ctx)
|
||||
}
|
||||
return h.ctrl.GetEnvironmentByName(ctx, environmentName)
|
||||
}
|
||||
|
||||
func (h *Handler) Run(ctx context.Context, req *entity.CommandRequest) error {
|
||||
isEphemeral := false
|
||||
|
||||
for _, arg := range req.Args {
|
||||
if arg == "--ephemeral" {
|
||||
isEphemeral = true
|
||||
}
|
||||
}
|
||||
|
||||
parsedArgs := make([]string, 0)
|
||||
|
||||
rgxEnvironment, err := regexp.Compile("--environment=(.*)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rgxService, err := regexp.Compile("--service=(.*)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetEnvironment := ""
|
||||
var targetServiceName *string
|
||||
|
||||
// Parse --environment={ENV} and --service={SERVICE} from args
|
||||
for _, arg := range req.Args {
|
||||
if matched := rgxEnvironment.FindStringSubmatch(arg); matched != nil {
|
||||
if len(matched) < 2 {
|
||||
return goErr.New("missing environment selection! \n(e.g --environment=production)")
|
||||
}
|
||||
targetEnvironment = matched[1]
|
||||
} else if matched := rgxService.FindStringSubmatch(arg); matched != nil {
|
||||
if len(matched) < 2 {
|
||||
return goErr.New("missing service selection! \n(e.g --service=serviceName)")
|
||||
}
|
||||
targetServiceName = &matched[1]
|
||||
} else {
|
||||
parsedArgs = append(parsedArgs, arg)
|
||||
}
|
||||
}
|
||||
|
||||
projectCfg, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.getEnvironment(ctx, targetEnvironment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add something to the ephemeral env name
|
||||
if isEphemeral {
|
||||
environmentName := fmt.Sprintf("%s-ephemeral", environment.Name)
|
||||
fmt.Printf("Spinning up Ephemeral Environment: %s\n", ui.BlueText(environmentName))
|
||||
// Create new environment for this run
|
||||
environment, err = h.ctrl.CreateEphemeralEnvironment(ctx, &entity.CreateEphemeralEnvironmentRequest{
|
||||
Name: environmentName,
|
||||
ProjectID: projectCfg.Project,
|
||||
BaseEnvironmentID: environment.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Done!")
|
||||
}
|
||||
envs, err := h.ctrl.GetEnvs(ctx, environment, targetServiceName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasDockerfile := true
|
||||
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/Dockerfile", pwd)); os.IsNotExist(err) {
|
||||
hasDockerfile = false
|
||||
}
|
||||
|
||||
if len(parsedArgs) == 0 && hasDockerfile {
|
||||
return h.runInDocker(ctx, pwd, envs)
|
||||
} else if len(parsedArgs) == 0 {
|
||||
return errors.CommandNotSpecified
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, parsedArgs[0], parsedArgs[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Inject railway envs
|
||||
for k, v := range *envs {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%+v", k, v))
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
catchSignals(ctx, cmd, nil)
|
||||
|
||||
err = cmd.Run()
|
||||
|
||||
if isEphemeral {
|
||||
// Teardown Environment
|
||||
fmt.Println("Tearing down ephemeral environment...")
|
||||
err := h.ctrl.DeleteEnvironment(ctx, &entity.DeleteEnvironmentRequest{
|
||||
EnvironmentId: environment.Id,
|
||||
ProjectID: projectCfg.Project,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Done!")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
os.Exit(exitError.ExitCode())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printLooksGood()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) runInDocker(ctx context.Context, pwd string, envs *entity.Envs) error {
|
||||
// Start building the image
|
||||
projectCfg, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strip characters not allowed in Docker image names
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sanitiser := regexp.MustCompile(`[^A-Za-z0-9_-]`)
|
||||
imageNameWithoutNsOrTag := strings.ToLower(sanitiser.ReplaceAllString(project.Name, "") + "-" + sanitiser.ReplaceAllString(environment.Name, ""))
|
||||
image := fmt.Sprintf("railway-local/%s:latest", imageNameWithoutNsOrTag)
|
||||
|
||||
buildArgs := []string{"build", "-q", "-t", image, pwd}
|
||||
|
||||
// Build up env
|
||||
for k, v := range *envs {
|
||||
buildArgs = append(buildArgs, "--build-arg", fmt.Sprintf("%s=\"%+v\"", k, v))
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "docker", buildArgs...)
|
||||
fmt.Printf("Building %s from Dockerfile...\n", ui.GreenText(image))
|
||||
|
||||
buildCmd.Stdout = os.Stdout
|
||||
buildCmd.Stderr = os.Stderr
|
||||
|
||||
err = buildCmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = buildCmd.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("🎉 Built %s\n", ui.GreenText(image))
|
||||
|
||||
// Attempt to use
|
||||
internalPort := envs.Get("PORT")
|
||||
|
||||
externalPort, err := getAvailablePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if internalPort == "" {
|
||||
internalPort = externalPort
|
||||
}
|
||||
|
||||
// Start running the image
|
||||
fmt.Printf("🚂 Running at %s\n\n", ui.GreenText(fmt.Sprintf("127.0.0.1:%s", externalPort)))
|
||||
|
||||
runArgs := []string{"run", "--init", "--rm", "-p", fmt.Sprintf("127.0.0.1:%s:%s", externalPort, internalPort), "-e", fmt.Sprintf("PORT=%s", internalPort), "-d"}
|
||||
// Build up env
|
||||
for k, v := range *envs {
|
||||
runArgs = append(runArgs, "-e", fmt.Sprintf("%s=%+v", k, v))
|
||||
}
|
||||
runArgs = append(runArgs, image)
|
||||
|
||||
// Run the container
|
||||
rawContainerId, err := exec.CommandContext(ctx, "docker", runArgs...).Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the container ID
|
||||
containerId := strings.TrimSpace(string(rawContainerId))
|
||||
|
||||
// Attach to the container
|
||||
logCmd := exec.CommandContext(ctx, "docker", "logs", "-f", containerId)
|
||||
logCmd.Stdout = os.Stdout
|
||||
logCmd.Stderr = os.Stderr
|
||||
|
||||
err = logCmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Listen for cancel to remove the container
|
||||
catchSignals(ctx, logCmd, func() {
|
||||
err = exec.Command("docker", "rm", "-f", string(containerId)).Run()
|
||||
})
|
||||
err = logCmd.Wait()
|
||||
if err != nil && !strings.Contains(err.Error(), "255") {
|
||||
// 255 is a graceeful exit with ctrl + c
|
||||
return err
|
||||
}
|
||||
|
||||
printLooksGood()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAvailablePort() (string, error) {
|
||||
searchRange := 64
|
||||
for i := RAIL_PORT; i < RAIL_PORT+searchRange; i++ {
|
||||
if isAvailable(i) {
|
||||
return strconv.Itoa(i), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Couldn't find available port between %d and %d", RAIL_PORT, RAIL_PORT+searchRange)
|
||||
}
|
||||
|
||||
func isAvailable(port int) bool {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = ln.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func catchSignals(_ context.Context, cmd *exec.Cmd, onSignal context.CancelFunc) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
err := cmd.Process.Signal(sig)
|
||||
if onSignal != nil {
|
||||
onSignal()
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("Child process error: \n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func printLooksGood() {
|
||||
// Get space between last output and this message
|
||||
fmt.Println()
|
||||
fmt.Printf(
|
||||
"🚄 Looks good? Then put it on the train and deploy with `%s`!\n",
|
||||
ui.GreenText("railway up"),
|
||||
)
|
||||
}
|
||||
60
cmd/shell.go
60
cmd/shell.go
|
|
@ -1,60 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Shell(ctx context.Context, req *entity.CommandRequest) error {
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shellVar := os.Getenv("SHELL")
|
||||
if shellVar == "" {
|
||||
// Fallback shell to use
|
||||
if isWindows() {
|
||||
shellVar = "cmd"
|
||||
} else {
|
||||
shellVar = "bash"
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(ui.Paragraph(fmt.Sprintf("Loading subshell with variables from %s", environment.Name)))
|
||||
|
||||
subShellCmd := exec.CommandContext(ctx, shellVar)
|
||||
subShellCmd.Env = os.Environ()
|
||||
for k, v := range *envs {
|
||||
subShellCmd.Env = append(subShellCmd.Env, fmt.Sprintf("%s=%+v", k, v))
|
||||
}
|
||||
|
||||
subShellCmd.Stdout = os.Stdout
|
||||
subShellCmd.Stderr = os.Stderr
|
||||
subShellCmd.Stdin = os.Stdin
|
||||
catchSignals(ctx, subShellCmd, nil)
|
||||
|
||||
err = subShellCmd.Run()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func isWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Status(ctx context.Context, req *entity.CommandRequest) error {
|
||||
projectCfg, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if project != nil {
|
||||
fmt.Printf("Project: %s\n", ui.Bold(fmt.Sprint(ui.MagentaText(project.Name))))
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Environment: %s\n", ui.Bold(fmt.Sprint(ui.BlueText(environment.Name))))
|
||||
|
||||
if len(project.Plugins) > 0 {
|
||||
fmt.Printf("Plugins:\n")
|
||||
for i := range project.Plugins {
|
||||
plugin := project.Plugins[i]
|
||||
if plugin.Name == "env" {
|
||||
// legacy plugin
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s\n", ui.Bold(fmt.Sprint(ui.GrayText(plugin.Name))))
|
||||
}
|
||||
}
|
||||
|
||||
if len(project.Services) > 0 {
|
||||
fmt.Printf("Services:\n")
|
||||
for i := range project.Services {
|
||||
fmt.Printf("%s\n", ui.Bold(fmt.Sprint(ui.GrayText(project.Services[i].Name))))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(errors.ProjectConfigNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"os"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Unlink(ctx context.Context, _ *entity.CommandRequest) error {
|
||||
projectCfg, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err == errors.ProjectConfigNotFound {
|
||||
fmt.Print(ui.AlertWarning("No project is currently linked"))
|
||||
os.Exit(1)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := h.ctrl.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.cfg.RemoveProjectConfigs(projectCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("🎉 Disconnected from %s\n", ui.MagentaText(project.Name))
|
||||
return nil
|
||||
}
|
||||
158
cmd/up.go
158
cmd/up.go
|
|
@ -1,158 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
CLIErrors "github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Up(ctx context.Context, req *entity.CommandRequest) error {
|
||||
isVerbose, err := req.Cmd.Flags().GetBool("verbose")
|
||||
if err != nil {
|
||||
// Verbose mode isn't a necessary flag; just default to false.
|
||||
isVerbose = false
|
||||
}
|
||||
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Using verbose mode"))
|
||||
|
||||
projectConfig, err := h.linkAndGetProjectConfigs(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src := projectConfig.ProjectPath
|
||||
if src == "" {
|
||||
// When deploying with a project token, the project path is empty
|
||||
src = "."
|
||||
}
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Uploading directory %s", src)))
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading environment"))
|
||||
environmentName, err := req.Cmd.Flags().GetString("environment")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.getEnvironment(ctx, environmentName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Using environment %s", ui.Bold(environment.Name))))
|
||||
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading project"))
|
||||
project, err := h.ctrl.GetProject(ctx, projectConfig.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceId := ""
|
||||
if serviceName != "" {
|
||||
for _, service := range project.Services {
|
||||
if service.Name == serviceName {
|
||||
serviceId = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if serviceId == "" {
|
||||
return CLIErrors.ServiceNotFound
|
||||
}
|
||||
}
|
||||
|
||||
// If service has not been provided via flag, prompt for it
|
||||
if serviceId == "" {
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Loading services"))
|
||||
service, err := ui.PromptServices(project.Services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if service != nil {
|
||||
serviceId = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
_, err = ioutil.ReadFile(".railwayignore")
|
||||
if err == nil {
|
||||
fmt.Print(ui.VerboseInfo(isVerbose, "Using ignore file .railwayignore"))
|
||||
}
|
||||
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Laying tracks in the clouds...",
|
||||
})
|
||||
res, err := h.ctrl.Upload(ctx, &entity.UploadRequest{
|
||||
ProjectID: projectConfig.Project,
|
||||
EnvironmentID: environment.Id,
|
||||
ServiceID: serviceId,
|
||||
RootDir: src,
|
||||
})
|
||||
if err != nil {
|
||||
ui.StopSpinner("")
|
||||
return err
|
||||
} else {
|
||||
ui.StopSpinner(fmt.Sprintf("☁️ Build logs available at %s\n", ui.GrayText(res.URL)))
|
||||
}
|
||||
detach, err := req.Cmd.Flags().GetBool("detach")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if detach {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err = h.ctrl.GetActiveBuildLogs(ctx, 0)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i) * 250 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n======= Build Completed ======\n\n")
|
||||
|
||||
err = h.ctrl.GetActiveDeploymentLogs(ctx, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("☁️ Deployment logs available at %s\n", ui.GrayText(res.URL))
|
||||
fmt.Printf("OR run `railway logs` to tail them here\n\n")
|
||||
|
||||
if res.DeploymentDomain != "" {
|
||||
fmt.Printf("☁️ Deployment live at %s\n", ui.GrayText(h.ctrl.GetFullUrlFromStaticUrl(res.DeploymentDomain)))
|
||||
} else {
|
||||
fmt.Printf("☁️ Deployment is live\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) linkAndGetProjectConfigs(ctx context.Context, req *entity.CommandRequest) (*entity.ProjectConfig, error) {
|
||||
projectConfig, err := h.ctrl.GetProjectConfigs(ctx)
|
||||
if err == CLIErrors.ProjectConfigNotFound {
|
||||
// If project isn't configured, prompt to link and do it again
|
||||
err := h.linkFromAccount(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectConfig, err = h.ctrl.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return projectConfig, nil
|
||||
}
|
||||
213
cmd/variables.go
213
cmd/variables.go
|
|
@ -1,213 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/cli/ui"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (h *Handler) Variables(ctx context.Context, req *entity.CommandRequest) error {
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(ui.Heading(fmt.Sprintf("%s Environment Variables", environment.Name)))
|
||||
fmt.Print(ui.KeyValues(*envs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) VariablesGet(ctx context.Context, req *entity.CommandRequest) error {
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range req.Args {
|
||||
fmt.Println(envs.Get(key))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) VariablesSet(ctx context.Context, req *entity.CommandRequest) error {
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
skipRedeploy, err := req.Cmd.Flags().GetBool("skip-redeploy")
|
||||
if err != nil {
|
||||
// The flag is optional; default to false.
|
||||
skipRedeploy = false
|
||||
}
|
||||
|
||||
replace, err := req.Cmd.Flags().GetBool("replace")
|
||||
if err != nil {
|
||||
// The flag is optional; default to false.
|
||||
replace = false
|
||||
}
|
||||
|
||||
yes, err := req.Cmd.Flags().GetBool("yes")
|
||||
if err != nil {
|
||||
// The flag is optional; default to false.
|
||||
yes = false
|
||||
}
|
||||
|
||||
if replace && !yes {
|
||||
fmt.Println(ui.Bold(ui.RedText(fmt.Sprintf("Warning! You are about to fully replace all your variables for the service '%s'.", serviceName)).String()))
|
||||
confirm, err := ui.PromptYesNo("Continue?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
variables := &entity.Envs{}
|
||||
updatedEnvNames := make([]string, 0)
|
||||
|
||||
for _, kvPair := range req.Args {
|
||||
parts := strings.SplitN(kvPair, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return errors.New("invalid variables invocation. See --help")
|
||||
}
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
variables.Set(key, value)
|
||||
updatedEnvNames = append(updatedEnvNames, key)
|
||||
}
|
||||
|
||||
err = h.ctrl.UpdateEnvs(ctx, variables, &serviceName, replace)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operation := "Updated"
|
||||
if replace {
|
||||
operation = "Replaced existing variables with"
|
||||
}
|
||||
|
||||
fmt.Print(ui.Heading(fmt.Sprintf("%s %s for \"%s\"", operation, strings.Join(updatedEnvNames, ", "), environment.Name)))
|
||||
fmt.Print(ui.KeyValues(*variables))
|
||||
|
||||
if !skipRedeploy {
|
||||
serviceID, err := h.ctrl.GetServiceIdByName(ctx, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.redeployAfterVariablesChange(ctx, environment, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) VariablesDelete(ctx context.Context, req *entity.CommandRequest) error {
|
||||
serviceName, err := req.Cmd.Flags().GetString("service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
skipRedeploy, err := req.Cmd.Flags().GetBool("skip-redeploy")
|
||||
if err != nil {
|
||||
// The flag is optional; default to false.
|
||||
skipRedeploy = false
|
||||
}
|
||||
|
||||
err = h.ctrl.DeleteEnvs(ctx, req.Args, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := h.ctrl.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(ui.Heading(fmt.Sprintf("Deleted %s for \"%s\"", strings.Join(req.Args, ", "), environment.Name)))
|
||||
|
||||
if !skipRedeploy {
|
||||
serviceID, err := h.ctrl.GetServiceIdByName(ctx, &serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.redeployAfterVariablesChange(ctx, environment, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) redeployAfterVariablesChange(ctx context.Context, environment *entity.Environment, serviceID *string) error {
|
||||
deployments, err := h.ctrl.GetDeployments(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't redeploy if we don't yet have any deployments
|
||||
if len(deployments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't redeploy if the latest deploy for environment came from up
|
||||
latestDeploy := deployments[0]
|
||||
if latestDeploy.Meta == nil || latestDeploy.Meta.Repo == "" {
|
||||
fmt.Printf(ui.AlertInfo("Run %s to redeploy your project"), ui.MagentaText("railway up").Underline())
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: fmt.Sprintf("Redeploying \"%s\" with new variables", environment.Name),
|
||||
})
|
||||
|
||||
err = h.ctrl.DeployEnvironmentTriggers(ctx, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.StopSpinner("Deploy triggered")
|
||||
|
||||
deployment, err := h.ctrl.GetLatestDeploymentForEnvironment(ctx, latestDeploy.ProjectID, environment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("☁️ Deploy Logs available at %s\n", ui.GrayText(h.ctrl.GetServiceDeploymentsURL(ctx, latestDeploy.ProjectID, *serviceID, deployment.ID)))
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/constants"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Version(ctx context.Context, req *entity.CommandRequest) error {
|
||||
fmt.Printf("railway version %s\n", ui.MagentaText(constants.Version))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) CheckVersion(ctx context.Context, req *entity.CommandRequest) error {
|
||||
if constants.Version != constants.VersionDefault {
|
||||
latest, _ := h.ctrl.GetLatestVersion()
|
||||
// Suppressing error as getting latest version is desired, not required
|
||||
if latest != "" && latest[1:] != constants.Version {
|
||||
fmt.Println(ui.Bold(fmt.Sprintf("A newer version of the Railway CLI is available, please update to: %s", ui.MagentaText(latest))))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (h *Handler) Whoami(ctx context.Context, req *entity.CommandRequest) error {
|
||||
user, err := h.ctrl.GetUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userText := fmt.Sprintf("%s", ui.MagentaText(user.Email))
|
||||
if user.Name != "" {
|
||||
userText = fmt.Sprintf("%s (%s)", user.Name, ui.MagentaText(user.Email))
|
||||
}
|
||||
fmt.Printf("👋 Hey %s\n", userText)
|
||||
|
||||
// Todo, more info, also more fun
|
||||
return nil
|
||||
}
|
||||
136
configs/main.go
136
configs/main.go
|
|
@ -1,136 +0,0 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/railwayapp/cli/constants"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
viper *viper.Viper
|
||||
configPath string
|
||||
}
|
||||
|
||||
type Configs struct {
|
||||
rootConfigs *Config
|
||||
projectConfigs *Config
|
||||
RailwayProductionToken string
|
||||
RailwayEnvFilePath string
|
||||
}
|
||||
|
||||
func IsDevMode() bool {
|
||||
environment, exists := os.LookupEnv("RAILWAY_ENV")
|
||||
return exists && environment == "develop"
|
||||
}
|
||||
|
||||
func IsStagingMode() bool {
|
||||
environment, exists := os.LookupEnv("RAILWAY_ENV")
|
||||
return exists && environment == "staging"
|
||||
}
|
||||
|
||||
func GetRailwayURL() string {
|
||||
url, exists := os.LookupEnv("RAILWAY_URL")
|
||||
if !exists {
|
||||
return constants.RAILWAY_URL
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (c *Configs) CreatePathIfNotExist(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Configs) marshalConfig(config *Config, cfg interface{}) error {
|
||||
reflectCfg := reflect.ValueOf(cfg)
|
||||
for i := 0; i < reflectCfg.NumField(); i++ {
|
||||
k := reflectCfg.Type().Field(i).Name
|
||||
v := reflectCfg.Field(i).Interface()
|
||||
|
||||
config.viper.Set(k, v)
|
||||
}
|
||||
|
||||
err := c.CreatePathIfNotExist(config.configPath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return config.viper.WriteConfig()
|
||||
}
|
||||
|
||||
func New() *Configs {
|
||||
// Configs stored in root (~/.railway)
|
||||
// Includes token, etc
|
||||
rootViper := viper.New()
|
||||
rootConfigPartialPath := ".railway/config.json"
|
||||
if IsDevMode() {
|
||||
rootConfigPartialPath = ".railway/dev-config.json"
|
||||
}
|
||||
|
||||
if IsStagingMode() {
|
||||
rootConfigPartialPath = ".railway/staging-config.json"
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rootConfigPath := path.Join(homeDir, rootConfigPartialPath)
|
||||
|
||||
rootViper.SetConfigFile(rootConfigPath)
|
||||
err = rootViper.ReadInConfig()
|
||||
if os.IsNotExist(err) {
|
||||
// That's okay, configs are created as needed
|
||||
} else if err != nil {
|
||||
fmt.Printf("Unable to parse railway config! %s\n", err)
|
||||
}
|
||||
|
||||
rootConfig := &Config{
|
||||
viper: rootViper,
|
||||
configPath: rootConfigPath,
|
||||
}
|
||||
|
||||
// Configs stored in projects (<project>/.railway)
|
||||
// Includes projectId, environmentId, etc
|
||||
projectDir, err := filepath.Abs("./.railway")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
projectViper := viper.New()
|
||||
|
||||
projectPath := path.Join(projectDir, "./config.json")
|
||||
projectViper.SetConfigFile(projectPath)
|
||||
err = projectViper.ReadInConfig()
|
||||
if os.IsNotExist(err) {
|
||||
// That's okay, configs are created as needed
|
||||
} else if err != nil {
|
||||
fmt.Printf("Unable to parse project config! %s\n", err)
|
||||
}
|
||||
|
||||
projectConfig := &Config{
|
||||
viper: projectViper,
|
||||
configPath: projectPath,
|
||||
}
|
||||
|
||||
return &Configs{
|
||||
projectConfigs: projectConfig,
|
||||
rootConfigs: rootConfig,
|
||||
RailwayProductionToken: os.Getenv("RAILWAY_TOKEN"),
|
||||
RailwayEnvFilePath: path.Join(projectDir, "env.json"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
func (c *Configs) getCWD() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cwd, nil
|
||||
}
|
||||
|
||||
func (c *Configs) GetProjectConfigs() (*entity.ProjectConfig, error) {
|
||||
// Ignore error because the config probably doesn't exist yet
|
||||
// TODO: Better error handling here
|
||||
|
||||
userCfg, err := c.GetRootConfigs()
|
||||
if err != nil {
|
||||
return nil, errors.RootConfigNotFound
|
||||
}
|
||||
|
||||
// lookup project in global config based on pwd
|
||||
cwd, err := c.getCWD()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find longest matching parent path
|
||||
var longestPath = -1
|
||||
var pathMatch = ""
|
||||
for path := range userCfg.Projects {
|
||||
var matches = strings.HasPrefix(fmt.Sprintf("%s/", cwd), fmt.Sprintf("%s/", path))
|
||||
if matches && len(path) > longestPath {
|
||||
longestPath = len(path)
|
||||
pathMatch = path
|
||||
}
|
||||
}
|
||||
|
||||
if longestPath == -1 {
|
||||
return nil, errors.ProjectConfigNotFound
|
||||
}
|
||||
|
||||
projectCfg, found := userCfg.Projects[pathMatch]
|
||||
|
||||
if !found {
|
||||
return nil, errors.ProjectConfigNotFound
|
||||
}
|
||||
|
||||
return &projectCfg, nil
|
||||
}
|
||||
|
||||
func (c *Configs) SetProjectConfigs(cfg *entity.ProjectConfig) error {
|
||||
rootCfg, err := c.GetRootConfigs()
|
||||
if err != nil {
|
||||
rootCfg = &entity.RootConfig{}
|
||||
}
|
||||
|
||||
if rootCfg.Projects == nil {
|
||||
rootCfg.Projects = make(map[string]entity.ProjectConfig)
|
||||
}
|
||||
|
||||
rootCfg.Projects[cfg.ProjectPath] = *cfg
|
||||
|
||||
return c.SetRootConfig(rootCfg)
|
||||
}
|
||||
|
||||
func (c *Configs) RemoveProjectConfigs(cfg *entity.ProjectConfig) error {
|
||||
rootCfg, err := c.GetRootConfigs()
|
||||
if err != nil {
|
||||
rootCfg = &entity.RootConfig{}
|
||||
}
|
||||
|
||||
delete(rootCfg.Projects, cfg.ProjectPath)
|
||||
|
||||
return c.SetRootConfig(rootCfg)
|
||||
}
|
||||
|
||||
func (c *Configs) createNewProjectConfig() (*entity.ProjectConfig, error) {
|
||||
cwd, err := c.getCWD()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectCfg := &entity.ProjectConfig{
|
||||
ProjectPath: cwd,
|
||||
}
|
||||
|
||||
return projectCfg, nil
|
||||
}
|
||||
|
||||
func (c *Configs) SetProject(projectID string) error {
|
||||
projectCfg, err := c.GetProjectConfigs()
|
||||
|
||||
if err != nil {
|
||||
projectCfg, err = c.createNewProjectConfig()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
projectCfg.Project = projectID
|
||||
return c.SetProjectConfigs(projectCfg)
|
||||
}
|
||||
|
||||
// SetNewProject configures railway project for current working directory
|
||||
func (c *Configs) SetNewProject(projectID string) error {
|
||||
projectCfg, err := c.createNewProjectConfig()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectCfg.Project = projectID
|
||||
return c.SetProjectConfigs(projectCfg)
|
||||
}
|
||||
|
||||
func (c *Configs) SetEnvironment(environmentId string) error {
|
||||
projectCfg, err := c.GetProjectConfigs()
|
||||
|
||||
if err != nil {
|
||||
projectCfg, err = c.createNewProjectConfig()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
projectCfg.Environment = environmentId
|
||||
return c.SetProjectConfigs(projectCfg)
|
||||
}
|
||||
|
||||
func (c *Configs) GetProject() (string, error) {
|
||||
projectCfg, err := c.GetProjectConfigs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return projectCfg.Project, nil
|
||||
}
|
||||
|
||||
func (c *Configs) GetCurrentEnvironment() (string, error) {
|
||||
projectCfg, err := c.GetProjectConfigs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return projectCfg.Environment, nil
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (c *Configs) GetRootConfigs() (*entity.RootConfig, error) {
|
||||
var cfg entity.RootConfig
|
||||
b, err := ioutil.ReadFile(c.rootConfigs.configPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errors.RootConfigNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(b, &cfg)
|
||||
return &cfg, err
|
||||
}
|
||||
|
||||
func (c *Configs) SetRootConfig(cfg *entity.RootConfig) error {
|
||||
if cfg.Projects == nil {
|
||||
cfg.Projects = make(map[string]entity.ProjectConfig)
|
||||
}
|
||||
|
||||
return c.marshalConfig(c.rootConfigs, *cfg)
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
func (c *Configs) GetUserConfigs() (*entity.UserConfig, error) {
|
||||
var rootCfg *entity.RootConfig
|
||||
rootCfg, err := c.GetRootConfigs()
|
||||
if err != nil {
|
||||
return nil, errors.UserConfigNotFound
|
||||
}
|
||||
|
||||
if rootCfg.User.Token == "" {
|
||||
return nil, errors.UserConfigNotFound
|
||||
}
|
||||
|
||||
return &rootCfg.User, nil
|
||||
}
|
||||
|
||||
func (c *Configs) SetUserConfigs(cfg *entity.UserConfig) error {
|
||||
var rootCfg *entity.RootConfig
|
||||
rootCfg, err := c.GetRootConfigs()
|
||||
if err != nil {
|
||||
rootCfg = &entity.RootConfig{}
|
||||
}
|
||||
|
||||
rootCfg.User = *cfg
|
||||
|
||||
return c.SetRootConfig(rootCfg)
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package constants
|
||||
|
||||
const RailwayDocsURL = "https://docs.railway.app"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package constants
|
||||
|
||||
const RailwayURLDefault = "https://railway.app"
|
||||
|
||||
var RAILWAY_URL string = RailwayURLDefault
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package constants
|
||||
|
||||
const VersionDefault = "Piped into LDflags on build. You are probably running Railway CLI from source."
|
||||
|
||||
var Version string = VersionDefault
|
||||
|
||||
func IsDevVersion() bool {
|
||||
return Version == VersionDefault
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (c *Controller) GetProjectConfigs(ctx context.Context) (*entity.ProjectConfig, error) {
|
||||
if c.cfg.RailwayProductionToken != "" {
|
||||
// Get project config from api
|
||||
projectToken, err := c.gtwy.GetProjectToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if projectToken != nil {
|
||||
return &entity.ProjectConfig{
|
||||
Project: projectToken.ProjectId,
|
||||
Environment: projectToken.EnvironmentId,
|
||||
LockedEnvsNames: map[string]bool{},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return c.cfg.GetProjectConfigs()
|
||||
}
|
||||
|
||||
func (c *Controller) PromptIfProtectedEnvironment(ctx context.Context) error {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val, ok := projectCfg.LockedEnvsNames[projectCfg.Environment]; ok && val {
|
||||
fmt.Println(ui.Bold(ui.RedText("Protected Environment Detected!").String()))
|
||||
confirm, err := ui.PromptYesNo("Continue?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (c *Controller) GetDeployments(ctx context.Context) ([]*entity.Deployment, error) {
|
||||
projectConfig, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.gtwy.GetDeploymentsForEnvironment(ctx, projectConfig.Project, projectConfig.Environment)
|
||||
}
|
||||
|
||||
func (c *Controller) GetActiveDeployment(ctx context.Context) (*entity.Deployment, error) {
|
||||
projectConfig, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deployment, err := c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectConfig.Project, projectConfig.Environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deployment, nil
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (c *Controller) DeployEnvironmentTriggers(ctx context.Context, serviceID *string) error {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.gtwy.DeployEnvironmentTriggers(ctx, &entity.DeployEnvironmentTriggersRequest{
|
||||
ProjectID: projectCfg.Project,
|
||||
EnvironmentID: projectCfg.Environment,
|
||||
ServiceID: *serviceID,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (c *Controller) Down(ctx context.Context, req *entity.DownRequest) error {
|
||||
err := c.gtwy.Down(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
CLIErrors "github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
// GetCurrentEnvironment returns the currently active environment for the Railway project
|
||||
func (c *Controller) GetCurrentEnvironment(ctx context.Context) (*entity.Environment, error) {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, environment := range project.Environments {
|
||||
if environment.Id == projectCfg.Environment {
|
||||
return environment, nil
|
||||
}
|
||||
}
|
||||
return nil, CLIErrors.EnvironmentNotSet
|
||||
}
|
||||
|
||||
func (c *Controller) GetEnvironmentByName(ctx context.Context, environmentName string) (*entity.Environment, error) {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, environment := range project.Environments {
|
||||
if environment.Name == environmentName {
|
||||
return environment, nil
|
||||
}
|
||||
}
|
||||
return nil, CLIErrors.EnvironmentNotFound
|
||||
}
|
||||
|
||||
func (c *Controller) CreateEnvironment(ctx context.Context, req *entity.CreateEnvironmentRequest) (*entity.Environment, error) {
|
||||
return c.gtwy.CreateEnvironment(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Controller) CreateEphemeralEnvironment(ctx context.Context, req *entity.CreateEphemeralEnvironmentRequest) (*entity.Environment, error) {
|
||||
return c.gtwy.CreateEphemeralEnvironment(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteEnvironment(ctx context.Context, req *entity.DeleteEnvironmentRequest) error {
|
||||
return c.gtwy.DeleteEnvironment(ctx, req)
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
CLIErrors "github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (c *Controller) GetEnvsForCurrentEnvironment(ctx context.Context, serviceName *string) (*entity.Envs, error) {
|
||||
environment, err := c.GetCurrentEnvironment(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetEnvs(ctx, environment, serviceName)
|
||||
}
|
||||
|
||||
func (c *Controller) GetEnvs(ctx context.Context, environment *entity.Environment, serviceName *string) (*entity.Envs, error) {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := c.GetCurrentProject(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get service id from name
|
||||
serviceId := ""
|
||||
|
||||
if serviceName != nil && *serviceName != "" {
|
||||
for _, service := range project.Services {
|
||||
if service.Name == *serviceName {
|
||||
serviceId = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if serviceId == "" {
|
||||
return nil, CLIErrors.ServiceNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if serviceId == "" {
|
||||
service, err := ui.PromptServices(project.Services)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if service != nil {
|
||||
serviceId = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := projectCfg.LockedEnvsNames[environment.Id]; ok && val {
|
||||
fmt.Println(ui.Bold(ui.RedText("Protected Environment Detected!").String()))
|
||||
confirm, err := ui.PromptYesNo("Continue fetching variables?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !confirm {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return c.gtwy.GetEnvs(ctx, &entity.GetEnvsRequest{
|
||||
ProjectID: projectCfg.Project,
|
||||
EnvironmentID: environment.Id,
|
||||
ServiceID: serviceId,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Controller) AutoImportDotEnv(ctx context.Context) error {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envFileLocation := fmt.Sprintf("%s/.env", dir)
|
||||
if _, err := os.Stat(envFileLocation); err == nil {
|
||||
// path/to/whatever does not exist
|
||||
shouldImportEnvs, err := ui.PromptYesNo("\n.env detected!\nImport your variables into Railway?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If the user doesn't want to import envs skip
|
||||
if !shouldImportEnvs {
|
||||
return nil
|
||||
}
|
||||
// Otherwise read .env and set envs
|
||||
err = godotenv.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envMap, err := godotenv.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(envMap) > 0 {
|
||||
return c.UpdateEnvs(ctx, (*entity.Envs)(&envMap), nil, false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) SaveEnvsToFile(ctx context.Context) error {
|
||||
envs, err := c.GetEnvsForCurrentEnvironment(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.cfg.CreatePathIfNotExist(c.cfg.RailwayEnvFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(envs, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(c.cfg.RailwayEnvFilePath, encoded, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateEnvs(ctx context.Context, envs *entity.Envs, serviceName *string, replace bool) error {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.PromptIfProtectedEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get service id from name
|
||||
serviceID := ""
|
||||
if serviceName != nil && *serviceName != "" {
|
||||
for _, service := range project.Services {
|
||||
if service.Name == *serviceName {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
return CLIErrors.ServiceNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
service, err := ui.PromptServices(project.Services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if service != nil {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
pluginID := ""
|
||||
|
||||
// If there is no service, use the env plugin
|
||||
if serviceID == "" {
|
||||
for _, p := range project.Plugins {
|
||||
if p.Name == "env" {
|
||||
pluginID = p.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.gtwy.UpdateVariablesFromObject(ctx, &entity.UpdateEnvsRequest{
|
||||
ProjectID: projectCfg.Project,
|
||||
EnvironmentID: projectCfg.Environment,
|
||||
PluginID: pluginID,
|
||||
ServiceID: serviceID,
|
||||
Envs: envs,
|
||||
Replace: replace,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteEnvs(ctx context.Context, names []string, serviceName *string) error {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.PromptIfProtectedEnvironment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get service id from name
|
||||
serviceID := ""
|
||||
if serviceName != nil && *serviceName != "" {
|
||||
for _, service := range project.Services {
|
||||
if service.Name == *serviceName {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
return CLIErrors.ServiceNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
service, err := ui.PromptServices(project.Services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if service != nil {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
pluginID := ""
|
||||
|
||||
// If there is no service, use the env plugin
|
||||
if serviceID == "" {
|
||||
for _, p := range project.Plugins {
|
||||
if p.Name == "env" {
|
||||
pluginID = p.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete each variable one by one
|
||||
for _, name := range names {
|
||||
err = c.gtwy.DeleteVariable(ctx, &entity.DeleteVariableRequest{
|
||||
ProjectID: projectCfg.Project,
|
||||
EnvironmentID: projectCfg.Environment,
|
||||
PluginID: pluginID,
|
||||
ServiceID: serviceID,
|
||||
Name: name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
const (
|
||||
GQL_SOFT_ERROR = "Error fetching build logs"
|
||||
)
|
||||
|
||||
func (c *Controller) GetActiveDeploymentLogs(ctx context.Context, numLines int32) error {
|
||||
deployment, err := c.GetActiveDeployment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.logsForState(ctx, &entity.DeploymentLogsRequest{
|
||||
DeploymentID: deployment.ID,
|
||||
ProjectID: deployment.ProjectID,
|
||||
NumLines: numLines,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Controller) GetActiveBuildLogs(ctx context.Context, numLines int32) error {
|
||||
projectConfig, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployment, err := c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectConfig.Project, projectConfig.Environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.logsForState(ctx, &entity.DeploymentLogsRequest{
|
||||
DeploymentID: deployment.ID,
|
||||
ProjectID: projectConfig.Project,
|
||||
NumLines: numLines,
|
||||
})
|
||||
}
|
||||
|
||||
/* Logs for state will get logs for a current state (Either building or not building state)
|
||||
It does this by capturing the initial state of the deploy, and looping while it stays in that state
|
||||
The loop captures the previous deploy as well as the current and does log diffing on the unified state
|
||||
When the state transitions from building to not building, the loop breaks
|
||||
*/
|
||||
func (c *Controller) logsForState(ctx context.Context, req *entity.DeploymentLogsRequest) error {
|
||||
// Stream on building -> Building until !Building then break
|
||||
// Stream on not building -> !Building until Failed then break
|
||||
deploy, err := c.gtwy.GetDeploymentByID(ctx, &entity.DeploymentByIDRequest{
|
||||
DeploymentID: req.DeploymentID,
|
||||
ProjectID: req.ProjectID,
|
||||
GQL: c.getQuery(ctx, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print Logs w/ Limit
|
||||
logLines := strings.Split(logsForState(ctx, deploy.Status, deploy), "\n")
|
||||
offset := 0.0
|
||||
if req.NumLines != 0 {
|
||||
// If a limit is set, walk it back n steps (with a min of zero so no panics)
|
||||
offset = math.Max(float64(len(logLines))-float64(req.NumLines)-1, 0.0)
|
||||
}
|
||||
// GQL may return partial errors for build logs if not ready
|
||||
// The response won't fail but will be a partial error. Check this.
|
||||
err = errFromGQL(ctx, logLines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output Initial Logs
|
||||
currLogs := strings.Join(logLines[int(offset):], "\n")
|
||||
if len(currLogs) > 0 {
|
||||
fmt.Println(currLogs)
|
||||
}
|
||||
|
||||
if deploy.Status == entity.STATUS_FAILED {
|
||||
return errors.New("Build Failed! Please see output for more information")
|
||||
}
|
||||
|
||||
prevDeploy := deploy
|
||||
logState := deploy.Status
|
||||
deltaState := hasTransitioned(nil, deploy)
|
||||
|
||||
for !deltaState && req.NumLines == 0 {
|
||||
time.Sleep(2 * time.Second)
|
||||
currDeploy, err := c.gtwy.GetDeploymentByID(ctx, &entity.DeploymentByIDRequest{
|
||||
DeploymentID: req.DeploymentID,
|
||||
ProjectID: req.ProjectID,
|
||||
GQL: c.getQuery(ctx, logState),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Current Logs fetched from server
|
||||
currLogs := strings.Split(logsForState(ctx, logState, currDeploy), "\n")
|
||||
// Previous logs fetched from prevDeploy variable
|
||||
prevLogs := strings.Split(logsForState(ctx, logState, prevDeploy), "\n")
|
||||
// Diff logs using the line numbers as references
|
||||
logDiff := currLogs[len(prevLogs)-1 : len(currLogs)-1]
|
||||
// If no changes we continue
|
||||
if len(logDiff) == 0 {
|
||||
continue
|
||||
}
|
||||
// Output logs
|
||||
fmt.Println(strings.Join(logDiff, "\n"))
|
||||
// Set out walk pointer forward using the newest logs
|
||||
deltaState = hasTransitioned(prevDeploy, currDeploy)
|
||||
prevDeploy = currDeploy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasTransitioned(prev *entity.Deployment, curr *entity.Deployment) bool {
|
||||
return prev != nil && curr != nil && prev.Status != curr.Status
|
||||
}
|
||||
|
||||
func (c *Controller) getQuery(ctx context.Context, status string) entity.DeploymentGQL {
|
||||
return entity.DeploymentGQL{
|
||||
BuildLogs: status == entity.STATUS_BUILDING || status == "",
|
||||
DeployLogs: status != entity.STATUS_BUILDING || status == "",
|
||||
Status: true,
|
||||
}
|
||||
}
|
||||
|
||||
func logsForState(ctx context.Context, status string, deploy *entity.Deployment) string {
|
||||
if status == entity.STATUS_BUILDING {
|
||||
return deploy.BuildLogs
|
||||
}
|
||||
return deploy.DeployLogs
|
||||
}
|
||||
|
||||
func errFromGQL(ctx context.Context, logLines []string) error {
|
||||
for _, l := range logLines {
|
||||
if strings.Contains(l, GQL_SOFT_ERROR) {
|
||||
return errors.New(GQL_SOFT_ERROR)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/railwayapp/cli/configs"
|
||||
"github.com/railwayapp/cli/gateway"
|
||||
"github.com/railwayapp/cli/random"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
gtwy *gateway.Gateway
|
||||
cfg *configs.Configs
|
||||
randomizer *random.Randomizer
|
||||
ghc *github.Client
|
||||
}
|
||||
|
||||
func New() *Controller {
|
||||
return &Controller{
|
||||
gtwy: gateway.New(),
|
||||
cfg: configs.New(),
|
||||
randomizer: random.New(),
|
||||
ghc: github.NewClient(nil),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/railwayapp/cli/constants"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
func (c *Controller) SendPanic(ctx context.Context, panicErr string, stacktrace string, command string) (bool, error) {
|
||||
confirmSendPanic()
|
||||
|
||||
projectCfg, err := c.cfg.GetProjectConfigs()
|
||||
if err != nil {
|
||||
return c.gtwy.SendPanic(ctx, &entity.PanicRequest{
|
||||
Command: command,
|
||||
PanicError: panicErr,
|
||||
Stacktrace: stacktrace,
|
||||
ProjectID: "",
|
||||
EnvironmentID: "",
|
||||
Version: constants.Version,
|
||||
})
|
||||
|
||||
}
|
||||
return c.gtwy.SendPanic(ctx, &entity.PanicRequest{
|
||||
Command: command,
|
||||
PanicError: panicErr,
|
||||
Stacktrace: stacktrace,
|
||||
ProjectID: projectCfg.Project,
|
||||
EnvironmentID: projectCfg.Environment,
|
||||
Version: constants.Version,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func confirmSendPanic() {
|
||||
fmt.Printf("🚨 Looks like something derailed, Press Enter to send error logs (^C to quit)")
|
||||
fmt.Fscanln(os.Stdin)
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: "Taking notes...",
|
||||
})
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (c *Controller) CreatePlugin(ctx context.Context, req *entity.CreatePluginRequest) (*entity.Plugin, error) {
|
||||
return c.gtwy.CreatePlugin(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Controller) GetAvailablePlugins(ctx context.Context, projectId string) ([]string, error) {
|
||||
plugins, err := c.gtwy.GetAvailablePlugins(ctx, projectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
CLIErrors "github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
// GetCurrentProject returns the currently active project
|
||||
func (c *Controller) GetCurrentProject(ctx context.Context) (*entity.Project, error) {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// GetProject returns a project of id projectId, error otherwise
|
||||
func (c *Controller) GetProject(ctx context.Context, projectId string) (*entity.Project, error) {
|
||||
return c.gtwy.GetProject(ctx, projectId)
|
||||
}
|
||||
|
||||
// GetProjectByName returns a project for the user of name projectName, error otherwise
|
||||
func (c *Controller) GetProjectByName(ctx context.Context, projectName string) (*entity.Project, error) {
|
||||
return c.gtwy.GetProjectByName(ctx, projectName)
|
||||
}
|
||||
|
||||
func (c *Controller) GetServiceIdByName(ctx context.Context, serviceName *string) (*string, error) {
|
||||
projectCfg, err := c.GetProjectConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.PromptIfProtectedEnvironment(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := c.GetProject(ctx, projectCfg.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get service id from name
|
||||
serviceID := ""
|
||||
if serviceName != nil && *serviceName != "" {
|
||||
for _, service := range project.Services {
|
||||
if service.Name == *serviceName {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
return nil, CLIErrors.ServiceNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if serviceID == "" {
|
||||
service, err := ui.PromptServices(project.Services)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if service != nil {
|
||||
serviceID = service.ID
|
||||
}
|
||||
}
|
||||
|
||||
return &serviceID, nil
|
||||
}
|
||||
|
||||
// CreateProject creates a project specified by the project request, error otherwise
|
||||
func (c *Controller) CreateProject(ctx context.Context, req *entity.CreateProjectRequest) (*entity.Project, error) {
|
||||
return c.gtwy.CreateProject(ctx, req)
|
||||
}
|
||||
|
||||
// CreateProjectFromTemplate creates a project from template specified by the project request, error otherwise
|
||||
func (c *Controller) CreateProjectFromTemplate(ctx context.Context, req *entity.CreateProjectFromTemplateRequest) (*entity.CreateProjectFromTemplateResult, error) {
|
||||
return c.gtwy.CreateProjectFromTemplate(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateProject updates a project specified by the project request, error otherwise
|
||||
func (c *Controller) UpdateProject(ctx context.Context, req *entity.UpdateProjectRequest) (*entity.Project, error) {
|
||||
return c.gtwy.UpdateProject(ctx, req)
|
||||
}
|
||||
|
||||
// GetProjects returns all projects associated with the user, error otherwise
|
||||
func (c *Controller) GetProjects(ctx context.Context) ([]*entity.Project, error) {
|
||||
return c.gtwy.GetProjects(ctx)
|
||||
}
|
||||
|
||||
// OpenProjectInBrowser opens the provided projectId in the browser
|
||||
func (c *Controller) OpenProjectInBrowser(ctx context.Context, projectID string, environmentID string) error {
|
||||
return c.gtwy.OpenProjectInBrowser(projectID, environmentID)
|
||||
}
|
||||
|
||||
// OpenProjectPathInBrowser opens the provided projectId with the provided path in the browser
|
||||
func (c *Controller) OpenProjectPathInBrowser(ctx context.Context, projectID string, environmentID string, path string) error {
|
||||
return c.gtwy.OpenProjectPathInBrowser(projectID, environmentID, path)
|
||||
}
|
||||
|
||||
// OpenProjectDeploymentsInBrowser opens the provided projectId's depolyments in the browser
|
||||
func (c *Controller) OpenProjectDeploymentsInBrowser(ctx context.Context, projectID string) error {
|
||||
return c.gtwy.OpenProjectDeploymentsInBrowser(projectID)
|
||||
}
|
||||
|
||||
// GetProjectDeploymentsURL returns the URL to access project deployment in browser
|
||||
func (c *Controller) GetProjectDeploymentsURL(ctx context.Context, projectID string) string {
|
||||
return c.gtwy.GetProjectDeploymentsURL(projectID)
|
||||
}
|
||||
|
||||
// GetServiceDeploymentsURL returns the URL to access service deployments in the browser
|
||||
func (c *Controller) GetServiceDeploymentsURL(ctx context.Context, projectID string, serviceID string, deploymentID string) string {
|
||||
return c.gtwy.GetServiceDeploymentsURL(projectID, serviceID, deploymentID)
|
||||
}
|
||||
|
||||
// GetLatestDeploymentForEnvironment returns the URL to access project deployment in browser
|
||||
func (c *Controller) GetLatestDeploymentForEnvironment(ctx context.Context, projectID string, environmentID string) (*entity.Deployment, error) {
|
||||
return c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectID, environmentID)
|
||||
}
|
||||
|
||||
func (c *Controller) OpenStaticUrlInBrowser(staticUrl string) error {
|
||||
return c.gtwy.OpenStaticUrlInBrowser(staticUrl)
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteProject(ctx context.Context, projectID string) error {
|
||||
return c.gtwy.DeleteProject(ctx, projectID)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// GetWritableGithubScopes creates a project specified by the project request, error otherwise
|
||||
func (c *Controller) GetWritableGithubScopes(ctx context.Context) ([]string, error) {
|
||||
return c.gtwy.GetWritableGithubScopes(ctx)
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
// GetStarters returns all available starters
|
||||
func (c *Controller) GetStarters(ctx context.Context) ([]*entity.Starter, error) {
|
||||
return c.gtwy.GetStarters(ctx)
|
||||
}
|
||||
209
controller/up.go
209
controller/up.go
|
|
@ -1,209 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
gitignore "github.com/railwayapp/cli/gateway"
|
||||
)
|
||||
|
||||
var validIgnoreFile = map[string]bool{
|
||||
".gitignore": true,
|
||||
".railwayignore": true,
|
||||
}
|
||||
|
||||
var skipDirs = []string{
|
||||
".git",
|
||||
"node_modules",
|
||||
}
|
||||
|
||||
type ignoreFile struct {
|
||||
prefix string
|
||||
ignore *gitignore.GitIgnore
|
||||
}
|
||||
|
||||
func scanIgnoreFiles(src string) ([]ignoreFile, error) {
|
||||
ignoreFiles := []ignoreFile{}
|
||||
|
||||
if err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
// no sense scanning for ignore files in skipped dirs
|
||||
for _, s := range skipDirs {
|
||||
if filepath.Base(path) == s {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fname := filepath.Base(path)
|
||||
if validIgnoreFile[fname] {
|
||||
igf, err := gitignore.CompileIgnoreFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix := filepath.Dir(path)
|
||||
if prefix == "." {
|
||||
prefix = "" // Handle root dir properly.
|
||||
}
|
||||
|
||||
ignoreFiles = append(ignoreFiles, ignoreFile{
|
||||
prefix: prefix,
|
||||
ignore: igf,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ignoreFiles, nil
|
||||
}
|
||||
|
||||
func compress(src string, buf io.Writer) error {
|
||||
// tar > gzip > buf
|
||||
zr := gzip.NewWriter(buf)
|
||||
tw := tar.NewWriter(zr)
|
||||
|
||||
// find all ignore files, including those in subdirs
|
||||
ignoreFiles, err := scanIgnoreFiles(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// walk through every file in the folder
|
||||
err = filepath.WalkDir(src, func(absoluteFile string, de os.DirEntry, passedErr error) error {
|
||||
relativeFile, err := filepath.Rel(src, absoluteFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if passedErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// follow symlinks by default
|
||||
resolvedFilePath, err := filepath.EvalSymlinks(absoluteFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get info about the file the link points at
|
||||
fileInfo, err := os.Lstat(resolvedFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
// skip directories if we can (for perf)
|
||||
// e.g., want to avoid walking node_modules dir
|
||||
for _, s := range skipDirs {
|
||||
if filepath.Base(relativeFile) == s {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ignoredFile := range ignoreFiles {
|
||||
if strings.HasPrefix(absoluteFile, ignoredFile.prefix) { // if ignore file applicable
|
||||
trimmed := strings.TrimPrefix(absoluteFile, ignoredFile.prefix)
|
||||
if ignoredFile.ignore.MatchesPath(trimmed) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read file into a buffer to prevent tar overwrites
|
||||
data := bytes.NewBuffer(nil)
|
||||
f, err := os.Open(resolvedFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(data, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// close the file to avoid hitting fd limit
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate tar headers
|
||||
header, err := tar.FileInfoHeader(fileInfo, resolvedFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// must provide real name
|
||||
// (see https://golang.org/src/archive/tar/common.go?#L626)
|
||||
header.Name = filepath.ToSlash(relativeFile)
|
||||
// size when we first observed the file
|
||||
header.Size = int64(data.Len())
|
||||
|
||||
// write header
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
// not a dir, write file content
|
||||
if _, err := io.Copy(tw, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// produce tar
|
||||
if err := tw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
// produce gzip
|
||||
if err := zr.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) Upload(
|
||||
ctx context.Context,
|
||||
req *entity.UploadRequest,
|
||||
) (*entity.UpResponse, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := compress(req.RootDir, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.gtwy.Up(ctx, &entity.UpRequest{
|
||||
Data: buf,
|
||||
ProjectID: req.ProjectID,
|
||||
EnvironmentID: req.EnvironmentID,
|
||||
ServiceID: req.ServiceID,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Controller) GetFullUrlFromStaticUrl(staticUrl string) string {
|
||||
return fmt.Sprintf("https://%s", staticUrl)
|
||||
}
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
configs "github.com/railwayapp/cli/configs"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
const (
|
||||
baseRailwayURL string = "https://railway.app"
|
||||
baseStagingURL string = "https://railway-staging.app"
|
||||
baseLocalhostURL string = "https://railway-develop.app"
|
||||
)
|
||||
|
||||
const (
|
||||
loginInvalidResponse string = "Invalid code"
|
||||
loginSuccessResponse string = "Ok"
|
||||
)
|
||||
|
||||
type LoginResponse struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
const maxAttempts = 2 * 60
|
||||
const pollInterval = 1 * time.Second
|
||||
|
||||
func (c *Controller) GetUser(ctx context.Context) (*entity.User, error) {
|
||||
userCfg, err := c.cfg.GetUserConfigs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userCfg.Token == "" {
|
||||
return nil, errors.UserConfigNotFound
|
||||
}
|
||||
return c.gtwy.GetUser(ctx)
|
||||
}
|
||||
|
||||
func (c *Controller) browserBasedLogin(ctx context.Context) (*entity.User, error) {
|
||||
var token string
|
||||
var returnedCode string
|
||||
port, err := c.randomizer.Port()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := c.randomizer.Code()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
srv := &http.Server{Addr: strconv.Itoa(port)}
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", getAPIURL())
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
token = r.URL.Query().Get("token")
|
||||
returnedCode = r.URL.Query().Get("code")
|
||||
|
||||
if code != returnedCode {
|
||||
res := LoginResponse{Error: loginInvalidResponse}
|
||||
byteRes, err := json.Marshal(&res)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
w.WriteHeader(400)
|
||||
_, err = w.Write(byteRes)
|
||||
if err != nil {
|
||||
fmt.Println("Invalid login response failed to serialize!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
res := LoginResponse{Status: loginSuccessResponse}
|
||||
byteRes, err := json.Marshal(&res)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
_, err = w.Write(byteRes)
|
||||
if err != nil {
|
||||
fmt.Println("Valid login response failed to serialize!")
|
||||
}
|
||||
} else if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, PATCH, POST, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil); err != nil {
|
||||
fmt.Println("Login server handshake failed!")
|
||||
}
|
||||
}()
|
||||
|
||||
url := getBrowserBasedLoginURL(port, code)
|
||||
err = c.ConfirmBrowserOpen("Logging in...", url)
|
||||
|
||||
if err != nil {
|
||||
// Opening the browser failed. Try browserless login
|
||||
return c.browserlessLogin(ctx)
|
||||
}
|
||||
|
||||
fmt.Println("No dice? Try railway login --browserless")
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if code != returnedCode {
|
||||
return nil, errors.LoginFailed
|
||||
}
|
||||
|
||||
err = c.cfg.SetUserConfigs(&entity.UserConfig{
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := c.gtwy.GetUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (c *Controller) pollForToken(ctx context.Context, code string) (string, error) {
|
||||
var count = 0
|
||||
for count < maxAttempts {
|
||||
token, err := c.gtwy.ConsumeLoginSession(ctx, code)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.LoginFailed
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
count++
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
return "", errors.LoginTimeout
|
||||
}
|
||||
|
||||
func (c *Controller) browserlessLogin(ctx context.Context) (*entity.User, error) {
|
||||
wordCode, err := c.gtwy.CreateLoginSession(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := getBrowserlessLoginURL(wordCode)
|
||||
|
||||
fmt.Printf("Your pairing code is: %s\n", ui.MagentaText(wordCode))
|
||||
fmt.Printf("To authenticate with Railway, please go to \n %s\n", url)
|
||||
|
||||
token, err := c.pollForToken(ctx, wordCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.cfg.SetUserConfigs(&entity.UserConfig{
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := c.gtwy.GetUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (c *Controller) Login(ctx context.Context, isBrowserless bool) (*entity.User, error) {
|
||||
// Invalidate current session if it exists
|
||||
if loggedIn, _ := c.IsLoggedIn(ctx); loggedIn {
|
||||
if err := c.gtwy.Logout(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if isBrowserless || isSSH() || isCodeSpaces() {
|
||||
return c.browserlessLogin(ctx)
|
||||
}
|
||||
|
||||
return c.browserBasedLogin(ctx)
|
||||
}
|
||||
|
||||
func (c *Controller) Logout(ctx context.Context) error {
|
||||
if loggedIn, _ := c.IsLoggedIn(ctx); !loggedIn {
|
||||
fmt.Printf("🚪 %s\n", ui.YellowText("Already logged out"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := c.gtwy.Logout(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.cfg.SetUserConfigs(&entity.UserConfig{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("👋 %s\n", ui.YellowText("Logged out"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) IsLoggedIn(ctx context.Context) (bool, error) {
|
||||
userCfg, err := c.cfg.GetUserConfigs()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
isLoggedIn := userCfg.Token != ""
|
||||
return isLoggedIn, nil
|
||||
}
|
||||
|
||||
func (c *Controller) ConfirmBrowserOpen(spinnerMsg string, url string) error {
|
||||
fmt.Printf("Press Enter to open the browser (^C to quit)")
|
||||
fmt.Fscanln(os.Stdin)
|
||||
ui.StartSpinner(&ui.SpinnerCfg{
|
||||
Message: spinnerMsg,
|
||||
})
|
||||
|
||||
err := browser.OpenURL(url)
|
||||
|
||||
if err != nil {
|
||||
ui.StopSpinner("Failed to open browser, attempting browserless login.")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAPIURL() string {
|
||||
if configs.IsDevMode() {
|
||||
return baseLocalhostURL
|
||||
}
|
||||
if configs.IsStagingMode() {
|
||||
return baseStagingURL
|
||||
}
|
||||
return baseRailwayURL
|
||||
}
|
||||
|
||||
func getHostName() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func getBrowserBasedLoginURL(port int, code string) string {
|
||||
hostname := getHostName()
|
||||
buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("port=%d&code=%s&hostname=%s", port, code, hostname)))
|
||||
url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer)
|
||||
return url
|
||||
}
|
||||
|
||||
func getBrowserlessLoginURL(wordCode string) string {
|
||||
hostname := getHostName()
|
||||
buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("wordCode=%s&hostname=%s", wordCode, hostname)))
|
||||
|
||||
url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer)
|
||||
return url
|
||||
}
|
||||
|
||||
func isSSH() bool {
|
||||
if os.Getenv("SSH_TTY") != "" || os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isCodeSpaces() bool {
|
||||
return os.Getenv("CODESPACES") == "true"
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func (c *Controller) GetLatestVersion() (string, error) {
|
||||
rep, _, err := c.ghc.Repositories.GetLatestRelease(context.Background(), "railwayapp", "cli")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *rep.TagName, nil
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
// GetWorkflowStatus fetches the status of a workflow based on request, error otherwise
|
||||
func (c *Controller) GetWorkflowStatus(ctx context.Context, workflowID string) (entity.WorkflowStatus, error) {
|
||||
return c.gtwy.GetWorkflowStatus(ctx, workflowID)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package entity
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type CommandRequest struct {
|
||||
Cmd *cobra.Command
|
||||
Args []string
|
||||
}
|
||||
|
||||
type CobraFunction func(cmd *cobra.Command, args []string) error
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package entity
|
||||
|
||||
type RootConfig struct {
|
||||
User UserConfig `json:"user"`
|
||||
Projects map[string]ProjectConfig `json:"projects"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ProjectConfig struct {
|
||||
ProjectPath string `json:"projectPath,omitempty"`
|
||||
Project string `json:"project,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
LockedEnvsNames map[string]bool `json:"lockedEnvsNames,omitempty"`
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package entity
|
||||
|
||||
const (
|
||||
STATUS_BUILDING = "BUILDING"
|
||||
STATUS_DEPLOYING = "DEPLOYING"
|
||||
STATUS_SUCCESS = "SUCCESS"
|
||||
STATUS_REMOVED = "REMOVED"
|
||||
STATUS_FAILED = "FAILED"
|
||||
)
|
||||
|
||||
type DeploymentMeta struct {
|
||||
Repo string `json:"repo"`
|
||||
Branch string `json:"branch"`
|
||||
CommitHash string `json:"commitHash"`
|
||||
CommitMessage string `json:"commitMessage"`
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"projectId"`
|
||||
BuildLogs string `json:"buildLogs"`
|
||||
DeployLogs string `json:"deployLogs"`
|
||||
Status string `json:"status"`
|
||||
StaticUrl string `json:"staticUrl"`
|
||||
Meta *DeploymentMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type DeploymentLogsRequest struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
DeploymentID string `json:"deploymentId"`
|
||||
NumLines int32 `json:"numLines"`
|
||||
}
|
||||
|
||||
type DeploymentGQL struct {
|
||||
ID bool `json:"id"`
|
||||
BuildLogs bool `json:"buildLogs"`
|
||||
DeployLogs bool `json:"deployLogs"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
type DeploymentByIDRequest struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
DeploymentID string `json:"deploymentId"`
|
||||
GQL DeploymentGQL
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package entity
|
||||
|
||||
type DeployEnvironmentTriggersRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
ServiceID string
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package entity
|
||||
|
||||
type DownRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package entity
|
||||
|
||||
type Environment struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type CreateEnvironmentRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ProjectID string `json:"projectId,omitempty"`
|
||||
}
|
||||
|
||||
type CreateEphemeralEnvironmentRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ProjectID string `json:"projectId,omitempty"`
|
||||
BaseEnvironmentID string `json:"baseEnvironmentId"`
|
||||
}
|
||||
|
||||
type DeleteEnvironmentRequest struct {
|
||||
EnvironmentId string `json:"environmentId,omitempty"`
|
||||
ProjectID string `json:"projectId,omitempty"`
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package entity
|
||||
|
||||
type GetEnvsRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
ServiceID string
|
||||
}
|
||||
|
||||
type GetEnvsForPluginRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
PluginID string
|
||||
}
|
||||
|
||||
type UpdateEnvsRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
PluginID string
|
||||
ServiceID string
|
||||
Envs *Envs
|
||||
Replace bool
|
||||
}
|
||||
|
||||
type DeleteVariableRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
PluginID string
|
||||
ServiceID string
|
||||
Name string
|
||||
}
|
||||
|
||||
type Envs map[string]string
|
||||
|
||||
func (e Envs) Get(name string) string {
|
||||
return e[name]
|
||||
}
|
||||
|
||||
func (e Envs) Set(name, value string) {
|
||||
e[name] = value
|
||||
}
|
||||
|
||||
func (e Envs) Has(name string) bool {
|
||||
_, ok := e[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e Envs) Delete(name string) {
|
||||
delete(e, name)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package entity
|
||||
|
||||
import "context"
|
||||
|
||||
type HandlerFunction func(context.Context, *CommandRequest) error
|
||||
|
||||
type PanicFunction func(context.Context, string, string, string, []string) error
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package entity
|
||||
|
||||
type PanicRequest struct {
|
||||
Command string
|
||||
PanicError string
|
||||
Stacktrace string
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
Version string
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package entity
|
||||
|
||||
type PluginList struct {
|
||||
Plugins []*Plugin `json:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type CreatePluginRequest struct {
|
||||
ProjectID string
|
||||
Plugin string
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package entity
|
||||
|
||||
type CreateProjectRequest struct {
|
||||
Name *string // Optional
|
||||
Description *string // Optional
|
||||
Plugins []string // Optional
|
||||
}
|
||||
|
||||
type CreateProjectFromTemplateRequest struct {
|
||||
Name string // Required
|
||||
Owner string // Required
|
||||
Template string // Required
|
||||
IsPrivate bool // Optional
|
||||
Plugins []string // Optional
|
||||
Variables map[string]string // Optional
|
||||
}
|
||||
|
||||
type UpdateProjectRequest struct {
|
||||
Id string // Required
|
||||
Name *string // Optional
|
||||
Description *string // Optional
|
||||
}
|
||||
|
||||
type CreateProjectFromTemplateResult struct {
|
||||
WorkflowID string
|
||||
ProjectID string
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||
Environments []*Environment `json:"environments,omitempty"`
|
||||
Plugins []*Plugin `json:"plugins,omitempty"`
|
||||
Team *string `json:"team,omitempty"`
|
||||
Services []*Service `json:"services,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectToken struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
EnvironmentId string `json:"environmentId"`
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package entity
|
||||
|
||||
type Service struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package entity
|
||||
|
||||
type Starter struct {
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type StarterEnvVar struct {
|
||||
Name string
|
||||
Desc string
|
||||
Default string
|
||||
Optional bool
|
||||
}
|
||||
27
entity/up.go
27
entity/up.go
|
|
@ -1,27 +0,0 @@
|
|||
package entity
|
||||
|
||||
import "bytes"
|
||||
|
||||
type UploadRequest struct {
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
ServiceID string
|
||||
RootDir string
|
||||
}
|
||||
|
||||
type UpRequest struct {
|
||||
Data bytes.Buffer
|
||||
ProjectID string
|
||||
EnvironmentID string
|
||||
ServiceID string
|
||||
}
|
||||
|
||||
type UpResponse struct {
|
||||
URL string
|
||||
DeploymentDomain string
|
||||
}
|
||||
|
||||
type UpErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"reqId"`
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package entity
|
||||
|
||||
type User struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Has2FA bool
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package entity
|
||||
|
||||
type WorkflowStatus string
|
||||
|
||||
func (s WorkflowStatus) IsError() bool {
|
||||
return s == "Error"
|
||||
}
|
||||
|
||||
func (s WorkflowStatus) IsRunning() bool {
|
||||
return s == "Running"
|
||||
}
|
||||
|
||||
func (s WorkflowStatus) IsComplete() bool {
|
||||
return s == "Complete"
|
||||
}
|
||||
|
||||
var (
|
||||
WorkflowRunning WorkflowStatus = "Running"
|
||||
WorkflowComplete WorkflowStatus = "Complete"
|
||||
WorkflowError WorkflowStatus = "Error"
|
||||
)
|
||||
|
||||
type WorkflowStatusResponse struct {
|
||||
Status WorkflowStatus `json:"status"`
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/ui"
|
||||
)
|
||||
|
||||
type RailwayError error
|
||||
|
||||
// TEST
|
||||
|
||||
var (
|
||||
RootConfigNotFound RailwayError = fmt.Errorf("Run %s to get started", ui.Bold("railway login"))
|
||||
UserConfigNotFound RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Not logged in."), ui.Bold("railway login"))
|
||||
ProjectConfigNotFound RailwayError = fmt.Errorf("%s\nRun %s to create a new project, or %s to use an existing project", ui.RedText("Project not found"), ui.Bold("railway init"), ui.Bold("railway link"))
|
||||
UserNotAuthorized RailwayError = fmt.Errorf("%s\nTry running %s", ui.RedText("Not authorized!"), ui.Bold("railway login"))
|
||||
ProjectTokenNotFound RailwayError = fmt.Errorf("%s\n", ui.RedText("Project token not found"))
|
||||
ProblemFetchingProjects RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem fetching your projects."))
|
||||
ProblemFetchingWritableGithubScopes RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem fetching GitHub metadata."))
|
||||
ProjectCreateFailed RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem creating the project."))
|
||||
ProjectCreateFromTemplateFailed RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem creating the project from template."))
|
||||
ProductionTokenNotSet RailwayError = fmt.Errorf("%s\nRun %s and head under `tokens` section. You can generate tokens to access Railway environment variables. Set that token in your environment as `RAILWAY_TOKEN=<insert token>` and you're all aboard!", ui.RedText("RAILWAY_TOKEN environment variable not set."), ui.Bold("railway open"))
|
||||
EnvironmentNotSet RailwayError = fmt.Errorf("%s", ui.RedText("No active environment found. Please select one"))
|
||||
EnvironmentNotFound RailwayError = fmt.Errorf("%s", ui.RedText("Environment does not exist on project. Specify an existing environment"))
|
||||
NoGitHubScopesFound RailwayError = fmt.Errorf("%s", ui.RedText("No GitHub organizations found. Please link your GitHub account to Railway and try again."))
|
||||
CommandNotSpecified RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Specify a command to run inside the railway environment. Not providing a command will build and run the Dockerfile in the current directory."), ui.Bold("railway run [cmd]"))
|
||||
LoginFailed RailwayError = fmt.Errorf("%s", ui.RedText("Login failed"))
|
||||
LoginTimeout RailwayError = fmt.Errorf("%s", ui.RedText("Login timeout"))
|
||||
PluginAlreadyExists RailwayError = fmt.Errorf("%s", ui.RedText("Plugin already exists"))
|
||||
PluginNotSpecified RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Specify a plugin to create."), ui.Bold("railway add <plugin>"))
|
||||
PluginCreateFailed RailwayError = fmt.Errorf("%s\nUhh Ohh! One of our trains derailed.", ui.RedText("There was a problem creating the plugin."))
|
||||
PluginGetFailed RailwayError = fmt.Errorf("%s\nUhh Ohh! One of our trains derailed.", ui.RedText("There was a problem getting plugins available for creation."))
|
||||
TelemetryFailed RailwayError = fmt.Errorf("%s", ui.RedText("One of our trains derailed. Any chance you can report this error on our Discord (https://railway.app/help)?"))
|
||||
WorkflowFailed RailwayError = fmt.Errorf("%s", ui.RedText("There was a problem deploying the project. Any chance you can report this error on our Discord (https://railway.app/help)?"))
|
||||
NoDeploymentsFound RailwayError = fmt.Errorf("%s", ui.RedText("No Deployments Found!"))
|
||||
DeploymentFetchingFailed RailwayError = fmt.Errorf("%s", "Failed to fetch deployments")
|
||||
CreateEnvironmentFailed RailwayError = fmt.Errorf("%s", ui.RedText("Creating environment failed!"))
|
||||
ServiceNotFound RailwayError = fmt.Errorf("%s", ui.RedText("Service not found in project"))
|
||||
)
|
||||
191
flake.lock
Normal file
191
flake.lock
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
{
|
||||
"nodes": {
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1677345087,
|
||||
"narHash": "sha256-PSkBGJ6KyGbTeLtEgdHSljm76NOeoww9hDgBD/QBffk=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "9a5b1008028e4b37e91f5951e639ad7848232f8e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1677370089,
|
||||
"narHash": "sha256-sdZ3ull2bldQYXK7tsX097C8JEuhBGIFPSfmA5nVoXE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "685e7494b02c6ac505482b1a471de4c285e87543",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1669833724,
|
||||
"narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "22.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1665296151,
|
||||
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"advisory-db": "advisory-db",
|
||||
"crane": "crane",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"crane",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"crane",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1676437770,
|
||||
"narHash": "sha256-mhJye91Bn0jJIE7NnEywGty/U5qdELfsT8S+FBjTdG4=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "a619538647bd03e3ee1d7b947f7c11ff289b376e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay_2": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1677465082,
|
||||
"narHash": "sha256-b82PmPWkt0pAsxmc477Yowq1Ez1VyjA5wnxE+yoIOWg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "2924bfce2fadc1ded4a2b8cfce7f2fd4ef41c36f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
122
flake.nix
Normal file
122
flake.nix
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
description = "Interact with Railway via CLI";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/22.11";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
advisory-db = {
|
||||
url = "github:rustsec/advisory-db";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, rust-overlay, nixpkgs, crane, flake-utils, advisory-db, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
toolchain = pkgs.rust-bin.stable.latest.default;
|
||||
|
||||
inherit (pkgs) lib;
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
src =
|
||||
let
|
||||
# Only keeps graphql files
|
||||
markdownFilter = path: _type: builtins.match ".*graphql$" path != null;
|
||||
markdownOrCargo = path: type:
|
||||
(markdownFilter path type) || (craneLib.filterCargoSources path type);
|
||||
in
|
||||
lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter = markdownOrCargo;
|
||||
};
|
||||
|
||||
# Common arguments can be set here to avoid repeating them later
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
pname = "railway";
|
||||
buildInputs = [
|
||||
# Add additional build inputs here
|
||||
] ++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
# Additional darwin specific inputs can be set here
|
||||
pkgs.libiconv
|
||||
pkgs.darwin.apple_sdk.frameworks.Security
|
||||
];
|
||||
|
||||
# Additional environment variables can be set directly
|
||||
# MY_CUSTOM_VAR = "some value";
|
||||
};
|
||||
|
||||
# Build *just* the cargo dependencies, so we can reuse
|
||||
# all of that work (e.g. via cachix) when running in CI
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
# Build the actual crate itself, reusing the dependency
|
||||
# artifacts from above.
|
||||
railway = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
|
||||
clippy = craneLib.cargoClippy (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||
});
|
||||
|
||||
audit = craneLib.cargoAudit (commonArgs // {
|
||||
inherit advisory-db;
|
||||
});
|
||||
|
||||
fmt = craneLib.cargoFmt (commonArgs // { });
|
||||
|
||||
test = craneLib.cargoNextest (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
partitions = 1;
|
||||
partitionType = "count";
|
||||
});
|
||||
in
|
||||
{
|
||||
checks = { };
|
||||
|
||||
packages = {
|
||||
default = railway;
|
||||
inherit clippy audit fmt test;
|
||||
};
|
||||
|
||||
apps = {
|
||||
default = flake-utils.lib.mkApp {
|
||||
drv = railway;
|
||||
};
|
||||
|
||||
clippy = flake-utils.lib.mkApp {
|
||||
drv = clippy;
|
||||
};
|
||||
|
||||
audit = flake-utils.lib.mkApp {
|
||||
drv = audit;
|
||||
};
|
||||
|
||||
fmt = flake-utils.lib.mkApp {
|
||||
drv = fmt;
|
||||
};
|
||||
|
||||
test = flake-utils.lib.mkApp {
|
||||
drv = test;
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default =
|
||||
import ./shell.nix {
|
||||
inherit pkgs;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
gqlgen "github.com/railwayapp/cli/lib/gql"
|
||||
)
|
||||
|
||||
func (g *Gateway) GetDeploymentsForEnvironment(ctx context.Context, projectId, environmentId string) ([]*entity.Deployment, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query ($projectId: ID!, $environmentId: ID!) {
|
||||
allDeploymentsForEnvironment(projectId: $projectId, environmentId: $environmentId) {
|
||||
id
|
||||
status
|
||||
projectId
|
||||
meta
|
||||
staticUrl
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", projectId)
|
||||
gqlReq.Var("environmentId", environmentId)
|
||||
|
||||
var resp struct {
|
||||
Deployments []*entity.Deployment `json:"allDeploymentsForEnvironment"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.DeploymentFetchingFailed
|
||||
}
|
||||
return resp.Deployments, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) GetLatestDeploymentForEnvironment(ctx context.Context, projectID, environmentID string) (*entity.Deployment, error) {
|
||||
deployments, err := g.GetDeploymentsForEnvironment(ctx, projectID, environmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(deployments) == 0 {
|
||||
return nil, errors.NoDeploymentsFound
|
||||
}
|
||||
for _, deploy := range deployments {
|
||||
if deploy.Status != entity.STATUS_REMOVED {
|
||||
return deploy, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.NoDeploymentsFound
|
||||
}
|
||||
|
||||
func (g *Gateway) GetDeploymentByID(ctx context.Context, req *entity.DeploymentByIDRequest) (*entity.Deployment, error) {
|
||||
gen, err := gqlgen.AsGQL(ctx, req.GQL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(`
|
||||
query ($projectId: ID!, $deploymentId: ID!) {
|
||||
deploymentById(projectId: $projectId, deploymentId: $deploymentId) {
|
||||
%s
|
||||
}
|
||||
}
|
||||
`, *gen))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("deploymentId", req.DeploymentID)
|
||||
|
||||
var resp struct {
|
||||
Deployment *entity.Deployment `json:"deploymentById"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.DeploymentFetchingFailed
|
||||
}
|
||||
return resp.Deployment, nil
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (g *Gateway) DeployEnvironmentTriggers(ctx context.Context, req *entity.DeployEnvironmentTriggersRequest) error {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($projectId: ID!, $environmentId: ID!, $serviceId: ID!) {
|
||||
deployEnvironmentTriggers(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("environmentId", req.EnvironmentID)
|
||||
gqlReq.Var("serviceId", req.ServiceID)
|
||||
|
||||
var resp struct {
|
||||
// Nothing useful here
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (g *Gateway) Down(ctx context.Context, req *entity.DownRequest) error {
|
||||
deployment, err := g.GetLatestDeploymentForEnvironment(ctx, req.ProjectID, req.EnvironmentID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation removeDeployment($projectId: ID!, $deploymentId: ID!) {
|
||||
removeDeployment(projectId: $projectId, deploymentId: $deploymentId)
|
||||
}
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("deploymentId", deployment.ID)
|
||||
|
||||
if err = gqlReq.Run(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
func (g *Gateway) CreateEnvironment(ctx context.Context, req *entity.CreateEnvironmentRequest) (*entity.Environment, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($name: String!, $projectId: String!) {
|
||||
createEnvironment(name: $name, projectId: $projectId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("name", req.Name)
|
||||
|
||||
var resp struct {
|
||||
Environment *entity.Environment `json:"createEnvironment,omitempty"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.CreateEnvironmentFailed
|
||||
}
|
||||
return resp.Environment, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) CreateEphemeralEnvironment(ctx context.Context, req *entity.CreateEphemeralEnvironmentRequest) (*entity.Environment, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($name: String!, $projectId: String!, $baseEnvironmentId: String!) {
|
||||
createEphemeralEnvironment(name: $name, projectId: $projectId, baseEnvironmentId: $baseEnvironmentId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("name", req.Name)
|
||||
gqlReq.Var("baseEnvironmentId", req.BaseEnvironmentID)
|
||||
|
||||
var resp struct {
|
||||
Environment *entity.Environment `json:"createEphemeralEnvironment,omitempty"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.CreateEnvironmentFailed
|
||||
}
|
||||
return resp.Environment, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) DeleteEnvironment(ctx context.Context, req *entity.DeleteEnvironmentRequest) error {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($environmentId: String!, $projectId: String!) {
|
||||
deleteEnvironment(environmentId: $environmentId, projectId: $projectId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("environmentId", req.EnvironmentId)
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
|
||||
var resp struct {
|
||||
Created bool `json:"createEnvironment,omitempty"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return errors.CreateEnvironmentFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (g *Gateway) GetEnvs(ctx context.Context, req *entity.GetEnvsRequest) (*entity.Envs, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query ($projectId: String!, $environmentId: String!, $serviceId: String!) {
|
||||
decryptedVariablesForService(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("environmentId", req.EnvironmentID)
|
||||
|
||||
if req.ServiceID != "" {
|
||||
gqlReq.Var("serviceId", req.ServiceID)
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Envs *entity.Envs `json:"decryptedVariablesForService"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Envs, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) UpdateVariablesFromObject(ctx context.Context, req *entity.UpdateEnvsRequest) error {
|
||||
queryName := "upsertVariablesFromObject"
|
||||
|
||||
if req.Replace {
|
||||
// When replacing, use the set query which will blow away all old variables and only set the ones in this query
|
||||
queryName = "variablesSetFromObject"
|
||||
}
|
||||
|
||||
gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(`
|
||||
mutation($projectId: String!, $environmentId: String!, $pluginId: String, $serviceId: String, $variables: Json!) {
|
||||
%s(projectId: $projectId, environmentId: $environmentId, pluginId: $pluginId, serviceId: $serviceId, variables: $variables)
|
||||
}
|
||||
`, queryName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("environmentId", req.EnvironmentID)
|
||||
if req.PluginID != "" {
|
||||
gqlReq.Var("pluginId", req.PluginID)
|
||||
}
|
||||
if req.ServiceID != "" {
|
||||
gqlReq.Var("serviceId", req.ServiceID)
|
||||
}
|
||||
|
||||
gqlReq.Var("variables", req.Envs)
|
||||
|
||||
if err := gqlReq.Run(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) DeleteVariable(ctx context.Context, req *entity.DeleteVariableRequest) error {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($projectId: String!, $environmentId: String!, $pluginId: String, $serviceId: String, $name: String!) {
|
||||
deleteVariable(projectId: $projectId, environmentId: $environmentId, pluginId: $pluginId, serviceId: $serviceId, name: $name)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("environmentId", req.EnvironmentID)
|
||||
gqlReq.Var("name", req.Name)
|
||||
if req.PluginID != "" {
|
||||
gqlReq.Var("pluginId", req.PluginID)
|
||||
}
|
||||
if req.ServiceID != "" {
|
||||
gqlReq.Var("serviceId", req.ServiceID)
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
ignore is a library which returns a new ignorer object which can
|
||||
test against various paths. This is particularly useful when trying
|
||||
to filter files based on a .gitignore document
|
||||
|
||||
The rules for parsing the input file are the same as the ones listed
|
||||
in the Git docs here: http://git-scm.com/docs/gitignore
|
||||
|
||||
The summarized version of the same has been copied here:
|
||||
|
||||
1. A blank line matches no files, so it can serve as a separator
|
||||
for readability.
|
||||
2. A line starting with # serves as a comment. Put a backslash ("\")
|
||||
in front of the first hash for patterns that begin with a hash.
|
||||
3. Trailing spaces are ignored unless they are quoted with backslash ("\").
|
||||
4. An optional prefix "!" which negates the pattern; any matching file
|
||||
excluded by a previous pattern will become included again. It is not
|
||||
possible to re-include a file if a parent directory of that file is
|
||||
excluded. Git doesn’t list excluded directories for performance reasons,
|
||||
so any patterns on contained files have no effect, no matter where they
|
||||
are defined. Put a backslash ("\") in front of the first "!" for
|
||||
patterns that begin with a literal "!", for example, "\!important!.txt".
|
||||
5. If the pattern ends with a slash, it is removed for the purpose of the
|
||||
following description, but it would only find a match with a directory.
|
||||
In other words, foo/ will match a directory foo and paths underneath it,
|
||||
but will not match a regular file or a symbolic link foo (this is
|
||||
consistent with the way how pathspec works in general in Git).
|
||||
6. If the pattern does not contain a slash /, Git treats it as a shell glob
|
||||
pattern and checks for a match against the pathname relative to the
|
||||
location of the .gitignore file (relative to the toplevel of the work
|
||||
tree if not from a .gitignore file).
|
||||
7. Otherwise, Git treats the pattern as a shell glob suitable for
|
||||
consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the
|
||||
pattern will not match a / in the pathname. For example,
|
||||
"Documentation/*.html" matches "Documentation/git.html" but not
|
||||
"Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html".
|
||||
8. A leading slash matches the beginning of the pathname. For example,
|
||||
"/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
|
||||
9. Two consecutive asterisks ("**") in patterns matched against full
|
||||
pathname may have special meaning:
|
||||
i. A leading "**" followed by a slash means match in all directories.
|
||||
For example, "** /foo" matches file or directory "foo" anywhere,
|
||||
the same as pattern "foo". "** /foo/bar" matches file or directory
|
||||
"bar" anywhere that is directly under directory "foo".
|
||||
ii. A trailing "/**" matches everything inside. For example, "abc/**"
|
||||
matches all files inside directory "abc", relative to the location
|
||||
of the .gitignore file, with infinite depth.
|
||||
iii. A slash followed by two consecutive asterisks then a slash matches
|
||||
zero or more directories. For example, "a/** /b" matches "a/b",
|
||||
"a/x/b", "a/x/y/b" and so on.
|
||||
iv. Other consecutive asterisks are considered invalid. */
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// An IgnoreParser is an interface which exposes a single method:
|
||||
// MatchesPath() - Returns true if the path is targeted by the patterns compiled
|
||||
// in the GitIgnore structure
|
||||
type IgnoreParser interface {
|
||||
MatchesPath(f string) bool
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// This function pretty much attempts to mimic the parsing rules
|
||||
// listed above at the start of this file
|
||||
func getPatternFromLine(line string) (*regexp.Regexp, bool) {
|
||||
// Trim OS-specific carriage returns.
|
||||
line = strings.TrimRight(line, "\r")
|
||||
|
||||
// Strip comments [Rule 2]
|
||||
if strings.HasPrefix(line, `#`) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Trim string [Rule 3]
|
||||
// TODO: Handle [Rule 3], when the " " is escaped with a \
|
||||
line = strings.Trim(line, " ")
|
||||
|
||||
// Exit for no-ops and return nil which will prevent us from
|
||||
// appending a pattern against this line
|
||||
if line == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// TODO: Handle [Rule 4] which negates the match for patterns leading with "!"
|
||||
negatePattern := false
|
||||
if line[0] == '!' {
|
||||
negatePattern = true
|
||||
line = line[1:]
|
||||
}
|
||||
|
||||
// Handle [Rule 2, 4], when # or ! is escaped with a \
|
||||
// Handle [Rule 4] once we tag negatePattern, strip the leading ! char
|
||||
if regexp.MustCompile(`^(\#|\!)`).MatchString(line) {
|
||||
line = line[1:]
|
||||
}
|
||||
|
||||
// If we encounter a foo/*.blah in a folder, prepend the / char
|
||||
if regexp.MustCompile(`([^\/+])/.*\*\.`).MatchString(line) && line[0] != '/' {
|
||||
line = "/" + line
|
||||
}
|
||||
|
||||
// Handle escaping the "." char
|
||||
line = regexp.MustCompile(`\.`).ReplaceAllString(line, `\.`)
|
||||
|
||||
magicStar := "#$~"
|
||||
|
||||
// Handle "/**/" usage
|
||||
if strings.HasPrefix(line, "/**/") {
|
||||
line = line[1:]
|
||||
}
|
||||
line = regexp.MustCompile(`/\*\*/`).ReplaceAllString(line, `(/|/.+/)`)
|
||||
line = regexp.MustCompile(`\*\*/`).ReplaceAllString(line, `(|.`+magicStar+`/)`)
|
||||
line = regexp.MustCompile(`/\*\*`).ReplaceAllString(line, `(|/.`+magicStar+`)`)
|
||||
|
||||
// Handle escaping the "*" char
|
||||
line = regexp.MustCompile(`\\\*`).ReplaceAllString(line, `\`+magicStar)
|
||||
line = regexp.MustCompile(`\*`).ReplaceAllString(line, `([^/]*)`)
|
||||
|
||||
// Handle escaping the "?" char
|
||||
line = strings.Replace(line, "?", `\?`, -1)
|
||||
|
||||
line = strings.Replace(line, magicStar, "*", -1)
|
||||
|
||||
// Temporary regex
|
||||
var expr = ""
|
||||
if strings.HasSuffix(line, "/") {
|
||||
expr = line + "(|.*)$"
|
||||
} else {
|
||||
expr = line + "(|/.*)$"
|
||||
}
|
||||
if strings.HasPrefix(expr, "/") {
|
||||
expr = "^(|/)" + expr[1:]
|
||||
} else {
|
||||
expr = "^(|.*/)" + expr
|
||||
}
|
||||
pattern, _ := regexp.Compile(expr)
|
||||
|
||||
return pattern, negatePattern
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// ignorePattern encapsulates a pattern and if it is a negated pattern.
|
||||
type ignorePattern struct {
|
||||
pattern *regexp.Regexp
|
||||
negate bool
|
||||
}
|
||||
|
||||
// GitIgnore wraps a list of ignore pattern.
|
||||
type GitIgnore struct {
|
||||
patterns []*ignorePattern
|
||||
}
|
||||
|
||||
// Accepts a variadic set of strings, and returns a GitIgnore object which
|
||||
// converts and appends the lines in the input to regexp.Regexp patterns
|
||||
// held within the GitIgnore objects "patterns" field
|
||||
func CompileIgnoreLines(lines ...string) (*GitIgnore, error) {
|
||||
gi := &GitIgnore{}
|
||||
for _, line := range lines {
|
||||
pattern, negatePattern := getPatternFromLine(line)
|
||||
if pattern != nil {
|
||||
ip := &ignorePattern{pattern, negatePattern}
|
||||
gi.patterns = append(gi.patterns, ip)
|
||||
}
|
||||
}
|
||||
return gi, nil
|
||||
}
|
||||
|
||||
// Accepts a ignore file as the input, parses the lines out of the file
|
||||
// and invokes the CompileIgnoreLines method
|
||||
func CompileIgnoreFile(fpath string) (*GitIgnore, error) {
|
||||
buffer, error := ioutil.ReadFile(fpath)
|
||||
if error == nil {
|
||||
s := strings.Split(string(buffer), "\n")
|
||||
return CompileIgnoreLines(s...)
|
||||
}
|
||||
|
||||
return CompileIgnoreLines("")
|
||||
}
|
||||
|
||||
// Accepts a ignore file as the input, parses the lines out of the file
|
||||
// and invokes the CompileIgnoreLines method with additional lines
|
||||
func CompileIgnoreFileAndLines(fpath string, lines ...string) (*GitIgnore, error) {
|
||||
buffer, error := ioutil.ReadFile(fpath)
|
||||
if error == nil {
|
||||
s := strings.Split(string(buffer), "\n")
|
||||
return CompileIgnoreLines(append(s, lines...)...)
|
||||
}
|
||||
return nil, error
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// MatchesPath returns true if the given GitIgnore structure would target
|
||||
// a given path string `f`.
|
||||
func (gi *GitIgnore) MatchesPath(f string) bool {
|
||||
// Replace OS-specific path separator.
|
||||
f = strings.Replace(f, string(os.PathSeparator), "/", -1)
|
||||
|
||||
matchesPath := false
|
||||
for _, ip := range gi.patterns {
|
||||
if ip.pattern.MatchString(f) {
|
||||
// If this is a regular target (not negated with a gitignore exclude "!" etc)
|
||||
if !ip.negate {
|
||||
matchesPath = true
|
||||
} else if matchesPath {
|
||||
// Negated pattern, and matchesPath is already set
|
||||
matchesPath = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchesPath
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
193
gateway/main.go
193
gateway/main.go
|
|
@ -1,193 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
errors2 "github.com/railwayapp/cli/errors"
|
||||
"github.com/railwayapp/cli/ui"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
configs "github.com/railwayapp/cli/configs"
|
||||
"github.com/railwayapp/cli/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
CLI_SOURCE_HEADER = "cli"
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
cfg *configs.Configs
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func GetHost() string {
|
||||
baseURL := "https://backboard.railway.app"
|
||||
if configs.IsDevMode() {
|
||||
baseURL = "https://backboard.railway-develop.app"
|
||||
}
|
||||
if configs.IsStagingMode() {
|
||||
baseURL = "https://backboard.railway-staging.app"
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
type AttachCommonHeadersTransport struct{}
|
||||
|
||||
func (t *AttachCommonHeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Add("x-source", CLI_SOURCE_HEADER)
|
||||
|
||||
version := constants.Version
|
||||
if constants.IsDevVersion() {
|
||||
version = "dev"
|
||||
}
|
||||
req.Header.Set("X-Railway-Version", version)
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func New() *Gateway {
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &AttachCommonHeadersTransport{},
|
||||
}
|
||||
|
||||
return &Gateway{
|
||||
cfg: configs.New(),
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
type GQLRequest struct {
|
||||
q string
|
||||
vars map[string]interface{}
|
||||
header http.Header
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type GQLError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e GQLError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
type GQLResponse struct {
|
||||
Errors []GQLError `json:"errors"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (g *Gateway) authorize(header http.Header) error {
|
||||
if g.cfg.RailwayProductionToken != "" {
|
||||
header.Add("project-access-token", g.cfg.RailwayProductionToken)
|
||||
} else {
|
||||
user, err := g.cfg.GetUserConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Add("authorization", fmt.Sprintf("Bearer %s", user.Token))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) NewRequestWithoutAuth(query string) *GQLRequest {
|
||||
gqlReq := &GQLRequest{
|
||||
q: query,
|
||||
header: http.Header{},
|
||||
httpClient: g.httpClient,
|
||||
vars: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
return gqlReq
|
||||
}
|
||||
|
||||
func (g *Gateway) NewRequestWithAuth(query string) (*GQLRequest, error) {
|
||||
gqlReq := g.NewRequestWithoutAuth(query)
|
||||
|
||||
err := g.authorize(gqlReq.header)
|
||||
if err != nil {
|
||||
return gqlReq, err
|
||||
}
|
||||
|
||||
return gqlReq, nil
|
||||
}
|
||||
|
||||
func (r *GQLRequest) Run(ctx context.Context, resp interface{}) error {
|
||||
var requestBody bytes.Buffer
|
||||
requestBodyObj := struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}{
|
||||
Query: r.q,
|
||||
Variables: r.vars,
|
||||
}
|
||||
if err := json.NewEncoder(&requestBody).Encode(requestBodyObj); err != nil {
|
||||
return errors.Wrap(err, "encode body")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/graphql", GetHost()), &requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
req.Header = r.header
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json; charset=utf-8")
|
||||
res, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, res.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Handle auth errors and other things in a special way
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return fmt.Errorf("Response not successful status=%d", res.StatusCode)
|
||||
}
|
||||
|
||||
gr := &GQLResponse{
|
||||
Data: resp,
|
||||
}
|
||||
if err := json.NewDecoder(&buf).Decode(&gr); err != nil {
|
||||
return errors.Wrap(err, "decoding response")
|
||||
}
|
||||
if len(gr.Errors) > 0 {
|
||||
messages := make([]string, len(gr.Errors))
|
||||
for i, err := range gr.Errors {
|
||||
messages[i] = err.Error()
|
||||
}
|
||||
|
||||
errText := gr.Errors[0].Message
|
||||
if len(gr.Errors) > 1 {
|
||||
errText = fmt.Sprintf("%d Errors: %s", len(gr.Errors), strings.Join(messages, ", "))
|
||||
}
|
||||
|
||||
// If any GQL responses return fail because unauthenticated, print an error telling the
|
||||
// user to log in and exit immediately
|
||||
if strings.Contains(errText, "Not Authorized") {
|
||||
println(ui.AlertDanger(errors2.UserNotAuthorized.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return errors.New(errText)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *GQLRequest) Var(name string, value interface{}) {
|
||||
r.vars[name] = value
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
func (g *Gateway) SendPanic(ctx context.Context, req *entity.PanicRequest) (bool, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($command: String!, $error: String!, $stacktrace: String!, $projectId: String, $environmentId: String) {
|
||||
sendTelemetry(command: $command, error: $error, stacktrace: $stacktrace, projectId: $projectId, environmentId: $environmentId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
gqlReq.Var("command", req.Command)
|
||||
gqlReq.Var("error", req.PanicError)
|
||||
gqlReq.Var("stacktrace", req.Stacktrace)
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("environmentId", req.EnvironmentID)
|
||||
|
||||
var resp struct {
|
||||
Status bool `json:"sendTelemetry"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return false, errors.TelemetryFailed
|
||||
}
|
||||
return resp.Status, nil
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
func (g *Gateway) GetAvailablePlugins(ctx context.Context, projectId string) ([]string, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query ($projectId: ID!) {
|
||||
availablePluginsForProject(projectId: $projectId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", projectId)
|
||||
|
||||
var resp struct {
|
||||
Plugins []string `json:"availablePluginsForProject"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.PluginGetFailed
|
||||
}
|
||||
return resp.Plugins, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) CreatePlugin(ctx context.Context, req *entity.CreatePluginRequest) (*entity.Plugin, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($projectId: String!, $name: String!) {
|
||||
createPlugin(projectId: $projectId, name: $name) {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.ProjectID)
|
||||
gqlReq.Var("name", req.Plugin)
|
||||
|
||||
var resp struct {
|
||||
Plugin *entity.Plugin `json:"createPlugin"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.PluginCreateFailed
|
||||
}
|
||||
return resp.Plugin, nil
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
configs "github.com/railwayapp/cli/configs"
|
||||
"github.com/railwayapp/cli/entity"
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
// GetProjectToken looks up a project and environment by the RAILWAY_TOKEN
|
||||
func (g *Gateway) GetProjectToken(ctx context.Context) (*entity.ProjectToken, error) {
|
||||
if g.cfg.RailwayProductionToken == "" {
|
||||
return nil, errors.ProjectTokenNotFound
|
||||
}
|
||||
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query {
|
||||
projectToken {
|
||||
projectId
|
||||
environmentId
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
ProjectToken *entity.ProjectToken `json:"projectToken"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProjectTokenNotFound
|
||||
}
|
||||
return resp.ProjectToken, nil
|
||||
}
|
||||
|
||||
// GetProject returns the project associated with the projectId, as well as
|
||||
// it's environments, plugins, etc
|
||||
func (g *Gateway) GetProject(ctx context.Context, projectId string) (*entity.Project, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query ($projectId: ID!) {
|
||||
projectById(projectId: $projectId) {
|
||||
id,
|
||||
name,
|
||||
plugins {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
environments {
|
||||
id,
|
||||
name
|
||||
},
|
||||
services {
|
||||
id,
|
||||
name
|
||||
},
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", projectId)
|
||||
|
||||
var resp struct {
|
||||
Project *entity.Project `json:"projectById"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProjectConfigNotFound
|
||||
}
|
||||
return resp.Project, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) GetProjectByName(ctx context.Context, projectName string) (*entity.Project, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query ($projectName: String!) {
|
||||
me {
|
||||
projects(where: { name: { equals: $projectName } }) {
|
||||
id,
|
||||
name,
|
||||
plugins {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
environments {
|
||||
id,
|
||||
name
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectName", projectName)
|
||||
|
||||
var resp struct {
|
||||
Me struct {
|
||||
Projects []*entity.Project `json:"projects"`
|
||||
} `json:"me"`
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProjectConfigNotFound
|
||||
}
|
||||
|
||||
projects := resp.Me.Projects
|
||||
if len(projects) == 0 {
|
||||
return nil, errors.ProjectConfigNotFound
|
||||
}
|
||||
|
||||
return projects[0], nil
|
||||
}
|
||||
|
||||
func (g *Gateway) CreateProject(ctx context.Context, req *entity.CreateProjectRequest) (*entity.Project, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($name: String) {
|
||||
createProject(name: $name) {
|
||||
id,
|
||||
name
|
||||
environments {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("name", req.Name)
|
||||
|
||||
var resp struct {
|
||||
Project *entity.Project `json:"createProject"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProjectCreateFailed
|
||||
}
|
||||
return resp.Project, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) CreateProjectFromTemplate(ctx context.Context, req *entity.CreateProjectFromTemplateRequest) (*entity.CreateProjectFromTemplateResult, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($name: String!, $owner: String!, $template: String!, $isPrivate: Boolean, $plugins: [String!], $variables: Json) {
|
||||
createProjectFromTemplate(name: $name, owner: $owner, template: $template, isPrivate: $isPrivate, plugins: $plugins, variables: $variables) {
|
||||
projectId
|
||||
workflowId
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("name", req.Name)
|
||||
gqlReq.Var("owner", req.Owner)
|
||||
gqlReq.Var("template", req.Template)
|
||||
gqlReq.Var("isPrivate", req.IsPrivate)
|
||||
gqlReq.Var("plugins", req.Plugins)
|
||||
gqlReq.Var("variables", req.Variables)
|
||||
|
||||
var resp struct {
|
||||
Result *entity.CreateProjectFromTemplateResult `json:"createProjectFromTemplate"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProjectCreateFromTemplateFailed
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) UpdateProject(ctx context.Context, req *entity.UpdateProjectRequest) (*entity.Project, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($projectId: ID!) {
|
||||
updateProject(projectId: $projectId) {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", req.Id)
|
||||
var resp struct {
|
||||
Project *entity.Project `json:"createProject"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Project, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) DeleteProject(ctx context.Context, projectId string) error {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
mutation($projectId: String!) {
|
||||
deleteProject(projectId: $projectId)
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gqlReq.Var("projectId", projectId)
|
||||
var resp struct {
|
||||
Deleted bool `json:"deleteProject"`
|
||||
}
|
||||
return gqlReq.Run(ctx, &resp)
|
||||
}
|
||||
|
||||
// GetProjects returns all projects associated with the user, as well as
|
||||
// their environments associated with those projects, error otherwise
|
||||
// Performs a dual join
|
||||
func (g *Gateway) GetProjects(ctx context.Context) ([]*entity.Project, error) {
|
||||
projectFrag := `
|
||||
id,
|
||||
updatedAt,
|
||||
name,
|
||||
plugins {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
environments {
|
||||
id,
|
||||
name
|
||||
},
|
||||
`
|
||||
|
||||
gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(`
|
||||
query {
|
||||
me {
|
||||
name
|
||||
projects {
|
||||
%s
|
||||
}
|
||||
teams {
|
||||
name
|
||||
projects {
|
||||
%s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, projectFrag, projectFrag))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Me struct {
|
||||
Name *string `json:"name"`
|
||||
Projects []*entity.Project `json:"projects"`
|
||||
Teams []*struct {
|
||||
Name string `json:"name"`
|
||||
Projects []*entity.Project `json:"projects"`
|
||||
} `json:"teams"`
|
||||
} `json:"me"`
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProblemFetchingProjects
|
||||
}
|
||||
|
||||
projects := resp.Me.Projects
|
||||
|
||||
for _, project := range resp.Me.Projects {
|
||||
name := "Me"
|
||||
if resp.Me.Name != nil {
|
||||
name = *resp.Me.Name
|
||||
}
|
||||
project.Team = &name
|
||||
}
|
||||
for _, team := range resp.Me.Teams {
|
||||
for _, project := range team.Projects {
|
||||
project.Team = &team.Name
|
||||
}
|
||||
projects = append(projects, team.Projects...)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) OpenProjectInBrowser(projectID string, environmentID string) error {
|
||||
return browser.OpenURL(fmt.Sprintf("%s/project/%s?environmentId=%s", configs.GetRailwayURL(), projectID, environmentID))
|
||||
}
|
||||
|
||||
func (g *Gateway) OpenProjectPathInBrowser(projectID string, environmentID string, path string) error {
|
||||
return browser.OpenURL(fmt.Sprintf("%s/project/%s/%s?environmentId=%s", configs.GetRailwayURL(), projectID, path, environmentID))
|
||||
}
|
||||
|
||||
func (g *Gateway) OpenProjectDeploymentsInBrowser(projectID string) error {
|
||||
return browser.OpenURL(g.GetProjectDeploymentsURL(projectID))
|
||||
}
|
||||
|
||||
func (g *Gateway) GetProjectDeploymentsURL(projectID string) string {
|
||||
return fmt.Sprintf("%s/project/%s/deployments?open=true", configs.GetRailwayURL(), projectID)
|
||||
}
|
||||
|
||||
func (g *Gateway) GetServiceDeploymentsURL(projectID string, serviceID string, deploymentID string) string {
|
||||
return fmt.Sprintf("%s/project/%s/service/%s?id=%s", configs.GetRailwayURL(), projectID, serviceID, deploymentID)
|
||||
}
|
||||
|
||||
func (g *Gateway) OpenStaticUrlInBrowser(staticUrl string) error {
|
||||
return browser.OpenURL(fmt.Sprintf("https://%s", staticUrl))
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/errors"
|
||||
)
|
||||
|
||||
// GetWritableGithubScopes returns scopes associated with Railway user
|
||||
func (g *Gateway) GetWritableGithubScopes(ctx context.Context) ([]string, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query {
|
||||
getWritableGithubScopes
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Scopes []string `json:"getWritableGithubScopes"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, errors.ProblemFetchingWritableGithubScopes
|
||||
}
|
||||
return resp.Scopes, nil
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (g *Gateway) GetStarters(ctx context.Context) ([]*entity.Starter, error) {
|
||||
gqlReq := g.NewRequestWithoutAuth(`
|
||||
query {
|
||||
getAllStarters {
|
||||
title
|
||||
url
|
||||
source
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
var resp struct {
|
||||
Starters []*entity.Starter `json:"getAllStarters"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
starters := resp.Starters
|
||||
|
||||
return starters, nil
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func constructReq(ctx context.Context, req *entity.UpRequest) (*http.Request, error) {
|
||||
url := fmt.Sprintf("%s/project/%s/environment/%s/up?serviceId=%s", GetHost(), req.ProjectID, req.EnvironmentID, req.ServiceID)
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, &req.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "multipart/form-data")
|
||||
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Up(ctx context.Context, req *entity.UpRequest) (*entity.UpResponse, error) {
|
||||
httpReq, err := constructReq(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = g.authorize(httpReq.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The `up` command uses a custom HTTP Client so there is no timeout on the requests
|
||||
client := &http.Client{
|
||||
Transport: &AttachCommonHeadersTransport{},
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
var res entity.UpErrorResponse
|
||||
// Try decoding up's error response and fallback to sending body as text if decoding fails
|
||||
if err := json.Unmarshal(bodyBytes, &res); err != nil {
|
||||
return nil, errors.New(string(bodyBytes))
|
||||
}
|
||||
return nil, errors.New(res.Message)
|
||||
}
|
||||
|
||||
var res entity.UpResponse
|
||||
if err := json.Unmarshal(bodyBytes, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/railwayapp/cli/entity"
|
||||
)
|
||||
|
||||
func (g *Gateway) GetUser(ctx context.Context) (*entity.User, error) {
|
||||
gqlReq, err := g.NewRequestWithAuth(`
|
||||
query {
|
||||
me {
|
||||
id,
|
||||
email,
|
||||
name,
|
||||
has2FA
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
User *entity.User `json:"me"`
|
||||
}
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.User, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) CreateLoginSession(ctx context.Context) (string, error) {
|
||||
gqlReq := g.NewRequestWithoutAuth(`mutation { createLoginSession } `)
|
||||
|
||||
var resp struct {
|
||||
Code string `json:"createLoginSession"`
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Code, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) ConsumeLoginSession(ctx context.Context, code string) (string, error) {
|
||||
gqlReq := g.NewRequestWithoutAuth(`
|
||||
mutation($code: String!) {
|
||||
consumeLoginSession(code: $code)
|
||||
}
|
||||
`)
|
||||
gqlReq.Var("code", code)
|
||||
|
||||
var resp struct {
|
||||
Token string `json:"consumeLoginSession"`
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Token, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Logout(ctx context.Context) error {
|
||||
gqlReq, err := g.NewRequestWithAuth(`mutation { logout }`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gqlReq.Run(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue