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:
Nebula 2023-03-03 21:44:32 -05:00 committed by GitHub
parent e3bd751b88
commit 0cfb79da46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
177 changed files with 9196 additions and 8223 deletions

11
.github/changelog-configuration.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"categories": [
{
"title": "## Changes",
"labels": [],
"exhaustive": false
}
],
"template": "${{CHANGELOG}}\n",
"pr_template": "- #${{NUMBER}} ${{TITLE}}"
}

View file

@ -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
View 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

View file

@ -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

View file

@ -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
View 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 }}

View file

@ -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
View file

@ -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

View file

@ -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 }}"

View file

@ -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

File diff suppressed because it is too large Load diff

75
Cargo.toml Normal file
View 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"

View file

@ -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.

View file

@ -1,5 +0,0 @@
build:
@go build -o bin/railway
run:
@go run main.go

View file

@ -1,57 +1,33 @@
# Railway CLI
# Railway CLI (3.x.x)
![Build](https://github.com/railwayapp/cli/workflows/Build/badge.svg)
[![CI](https://github.com/railwayapp/cliv3/actions/workflows/ci.yml/badge.svg)](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.

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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(),
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"),
)
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"),
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -1,3 +0,0 @@
package constants
const RailwayDocsURL = "https://docs.railway.app"

View file

@ -1,5 +0,0 @@
package constants
const RailwayURLDefault = "https://railway.app"
var RAILWAY_URL string = RailwayURLDefault

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,
})
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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),
}
}

View file

@ -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...",
})
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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"`
}

View file

@ -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
}

View file

@ -1,7 +0,0 @@
package entity
type DeployEnvironmentTriggersRequest struct {
ProjectID string
EnvironmentID string
ServiceID string
}

View file

@ -1,6 +0,0 @@
package entity
type DownRequest struct {
ProjectID string
EnvironmentID string
}

View file

@ -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"`
}

View file

@ -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)
}

View file

@ -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

View file

@ -1,10 +0,0 @@
package entity
type PanicRequest struct {
Command string
PanicError string
Stacktrace string
ProjectID string
EnvironmentID string
Version string
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -1,6 +0,0 @@
package entity
type Service struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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
View 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
View 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;
};
});
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 doesnt 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
}
////////////////////////////////////////////////////////////

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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