def plan_repo_add(config, plan_id, repo): api_client = ApiClient(config) # Look up the plan plan = get_plan(api_client, plan_id) # Filter by repository repo_data = lookup_repo(api_client, config, repo, required=True) # Check that we don't already have a matching PlanRepository params = { 'plan': plan_id, 'repo': repo_data['id'], } res = api_client('plan_repos', 'list', params=params) if res['count']: raise click.ClickException('Repo already added to plan') # Create the PlanRepository params = { 'plan_id': plan_id, 'repo_id': repo_data['id'], } res = api_client('plan_repos', 'create', params=params) click.echo() click.echo( 'Added repo {repo[owner]}/{repo[name]} to plan {plan[name]}'.format( **res)) render_recursive(res)
def build_info(config, build_id, log, flow_log, flow): api_client = ApiClient(config) params = { 'id': build_id, } # Look up the build try: build_res = api_client('builds', 'read', params=params) except coreapi.exceptions.ErrorMessage as e: raise click.ClickException( 'Build with id {} not found. Use metaci build list to see a list of latest builds and their ids' .format(build_id)) if log: click.echo(build_res['log']) elif flow_log: params = {'build': build_id} if flow: params['flow'] = flow build_flow_res = api_client('build_flows', 'list', params=params) for build_flow in build_flow_res['results']: click.echo() click.echo( click.style('{}:'.format(build_flow['flow']), bold=True, fg='blue')) click.echo(build_flow['log']) else: click.echo(render_recursive(build_res))
def service_info(config, name): api_client = ApiClient(config) params = {'name': name} # Look up the service res = api_client('services', 'list', params=params) if res['count'] == 0: raise click.ClickError('Service named {} not found'.format(name)) click.echo(render_recursive(res['results'][0]))
def org_info(config, name, repo): api_client = ApiClient(config) params = {'name': name} # Filter by repository repo_data = lookup_repo(api_client, config, repo, required=True) params['repo'] = repo_data['id'] # Look up the org res = api_client('orgs', 'list', params=params) if res['count'] == 0: raise click.ClickError('Org named {} not found'.format(name)) click.echo(render_recursive(res['results'][0]))
def plan_add(config): if not config.project_config: raise click.UsageError( 'You must be inside a local git repository configured for CumulusCI. No CumulusCI configuration was detected in the local directory' ) api_client = ApiClient(config) params = {} # Make sure the local repo exists repo_data = lookup_repo(api_client, config, None, required=True) click.echo('# Name and Description') click.echo( 'Provide a name and description of the build plan you are creating') name = click.prompt('Name') description = click.prompt('Description') click.echo() click.echo('# Org') click.echo( 'Select the MetaCI org this plan should run its builds against. If the org does not yet exist, use metaci org create to create the org first.' ) org = prompt_org(api_client, config, repo_data['id']) flows_list = config.project_config.flows.keys() flows_list.sort() click.echo() click.echo('# Flows') click.echo( 'What CumulusCI flows should this plan run? You can enter multiple flows using a comma as a separator' ) click.echo('Available Flows: {}'.format(', '.join(flows_list))) flows = click.prompt('Flow(s)') click.echo() click.echo('# Trigger Type') click.echo('How should this plan be triggered?') click.echo( ' - commit: Trigger on branch commits matching a regex pattern') click.echo(' - tag: Trigger on new tags matching a regex pattern') click.echo( ' - manual: Do not auto-trigger any builds. Can be manually triggered.' ) trigger_type = click.prompt('Trigger Type (commit, tag, or manual)') if trigger_type == 'commit': regex = click.prompt('Branch Match RegEx (ex feature/.*)') elif trigger_type == 'tag': regex = click.prompt('Tag Match RegEx (ex beta/.*)') elif trigger_type != 'manual': raise click.UsageError( '{} is an invalid trigger type. Valid choices are: commit, tag, manual' ) else: regex = None click.echo() click.echo('# Github Commit Status') click.echo( 'MetaCI can set the build status via the Github Commit Status API. Commit statuses are grouped by a context field. Setting a different context on different plans will cause multiple commit statuses to be created for each unique context in Github.' ) context = None if click.confirm('Set commit status in Github for this plan?', default=True): context = click.prompt('Github Commit Status Context', default=name) if trigger_type == 'manual': active = True else: click.echo() click.echo('# Activate Plan') match_str = 'matching regex {}'.format(regex) if regex else '' click.echo( 'If active, this plan will automatically build on new {}s{}'. format(trigger_type, match_str)) active = click.confirm('Active?', default=True) click.echo() click.echo('# Public or Private') click.echo( 'Public plans and their builds are visible to anonymous users. This is recommended for open source projects, instances on an internal network or VPN, or projects that want transparency of their build information. Private plans are only visible to logged in users who have been granted the Is Staff role by an admin.' ) public = click.confirm('Public?') params = { 'name': name, 'description': description, 'org': org, 'flows': flows, 'type': trigger_type, 'regex': regex, 'context': context, 'active': active, 'public': public, } res = api_client('plans', 'create', params=params) click.echo() click.echo( click.style( 'Created plan {} with the following configuration'.format(name), fg='green', )) render_recursive(res) click.echo() click.echo('Adding repository {owner}/{name} to plan'.format(**repo_data)) params = { 'repo_id': repo_data['id'], 'plan_id': res['id'], } res = api_client('plan_repos', 'create', params=params)
def plan_info(config, plan_id): api_client = ApiClient(config) plan = get_plan(api_client, plan_id) click.echo(render_recursive(plan))
def site_info(config): service = check_current_site(config) render_recursive(service.config)
def site_add(config, name, shape): if not config.project_config: raise click.ClickException('You must be in a CumulusCI configured local git repository.') verify_overwrite(config) # Initialize payload payload = { 'app': {}, 'source_blob': { 'url': 'https://github.com/SFDO-Tooling/MetaCI/tarball/master/', }, } env = {} # Heroku API Token try: token = subprocess.check_output(['heroku','auth:token']).strip() except: click.echo() click.echo(click.style('# Heroku API Token', bold=True, fg='blue')) click.echo('Enter your Heroku API Token. If you do not have a token, go to the Account page in Heroku and use the API Token section: https://dashboard.heroku.com/account') click.echo(click.style( 'NOTE: For security purposes, your input will be hidden. Paste your API Token and hit Enter to continue.', fg='yellow', )) token = click.prompt('API Token', hide_input=True) heroku_api = prompt_heroku_token() # App Name if not name: click.echo() click.echo(click.style('# Heroku App Name', bold=True, fg='blue')) click.echo('Specify the name of the Heroku app you want to create.') payload['app']['name'] = click.prompt('App Name') else: payload['app']['name'] = name app_shape, num_workers = prompt_app_shape(shape) # Hirefire Token if app_shape == 'prod': click.echo() click.echo(click.style('# Hirefire Token', bold=True, fg='blue')) click.echo('The prod app shape requires the use of Hirefire.io to scale the build worker dynos. You will need to have an account on Hirefire.io and get the API token from your account.') env['HIREFIRE_TOKEN'] = click.prompt('Hirefire API Token') # Salesforce DX click.echo() click.echo(click.style('# Salesforce DX Configuration', bold=True, fg='blue')) click.echo('The following prompts collect information from your local Salesforce DX configuration to use to configure MetaCI to use sfdx') # Salesforce DX Hub Key click.echo() click.echo('MetaCI uses JWT to connect to your Salesforce DX devhub. Please enter the path to your local private key file. If you have not set up JWT for your devhub, refer to the documentation: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm') sfdx_private_key = click.prompt( 'Path to private key', type=click.Path(readable=True, dir_okay=False), default=os.path.expanduser('~/.ssh/sfdx_server.key'), ) with open(sfdx_private_key, 'r') as f: env['SFDX_HUB_KEY'] = f.read() # Salesforce DX Client ID click.echo() click.echo('Enter the Connected App Client ID you used for the JWT authentication flow to the Salesforce DX devhub.') click.echo(click.style( 'NOTE: For security purposes, your input will be hidden. Paste your Client ID and hit Enter to continue.', fg='yellow', )) env['SFDX_CLIENT_ID'] = click.prompt('Client ID', hide_input=True) # Salesforce DX Username try: devhub = subprocess.check_output(['sfdx','force:config:get', 'defaultdevhubusername', '--json']).strip() devhub = json.loads(devhub) devhub = devhub['result'][0]['value'] except: devhub = None if devhub: env['SFDX_HUB_USERNAME'] = devhub else: click.echo() click.echo('Enter the username MetaCI should use for JWT authentication to the Salesforce DX devhub.') env['SFDX_HUB_USERNAME'] = click.prompt('Username') # Get connected app info from CumulusCI keychain connected_app = config.keychain.get_service('connected_app') env['CONNECTED_APP_CALLBACK_URL'] = connected_app.callback_url env['CONNECTED_APP_CLIENT_ID'] = connected_app.client_id env['CONNECTED_APP_CLIENT_SECRET'] = connected_app.client_secret # Set the site url env['SITE_URL'] = 'https://{}.herokuapp.com'.format(payload['app']['name']) # Get Github credentials from CumulusCI keychain github = config.keychain.get_service('github') env['GITHUB_USERNAME'] = github.username env['GITHUB_PASSWORD'] = github.password env['GITHUB_WEBHOOK_BASE_URL'] = '{SITE_URL}/webhook/github'.format(**env) env['FROM_EMAIL'] = github.email # Prepare the payload payload['overrides'] = { 'env': env, } # Create the new Heroku App headers = { 'Accept': 'application/vnd.heroku+json; version=3', 'Authorization': 'Bearer {}'.format(token), } resp = requests.post('https://api.heroku.com/app-setups', json=payload, headers=headers) if resp.status_code != 202: raise click.ClickException('Failed to create Heroku App. Reponse code [{}]: {}'.format(resp.status_code, resp.json())) app_setup = resp.json() # Check status status = app_setup['status'] click.echo() click.echo('Status: {}'.format(status)) click.echo(click.style('Creating app:', fg='yellow')) i = 1 build_started = False with click.progressbar(length=200) as bar: while status in ['pending']: check_resp = requests.get( 'https://api.heroku.com/app-setups/{id}'.format(**app_setup), headers=headers, ) if check_resp.status_code != 200: raise click.ClickException('Failed to check status of app creation. Reponse code [{}]: {}'.format(check_resp.status_code, check_resp.json())) check_data = check_resp.json() # Stream the build log output once the build starts if not build_started and check_data['build'] != None: build_started = True bar.update(100) click.echo() click.echo() click.echo(click.style('Build {id} Started:'.format(**check_data['build']), fg='yellow')) build_stream = requests.get(check_data['build']['output_stream_url'], stream=True, headers=headers) for chunk in build_stream.iter_content(): click.echo(chunk, nl=False) # If app-setup is still pending, sleep and then poll again if check_data['status'] != 'pending': bar.update(100) break if i < 98: i += 1 bar.update(i) time.sleep(2) click.echo() # Success if check_data['status'] == 'succeeded': click.echo(click.style('Heroku app creation succeeded!', fg='green', bold=True)) render_recursive(check_data) # Failed elif check_data['status'] == 'failed': click.echo(click.style('Heroku app creation failed', fg='red', bold=True)) render_recursive(check_data) if check_data['build']: click.echo() click.echo('Build Info:') resp = requests.get('https://api.heroku.com/builds/{id}'.format(**check_data['build']), headers=headers) render_recursive(resp.json()) return else: raise click.ClickException('Received an unknown status from the Heroku app-setups API. Full API response: {}'.format(check_data)) heroku_app = heroku_api.app(check_data['app']['id']) # Apply the app shape click.echo() click.echo(click.style('# Applying App Shape', bold=True, fg='blue')) set_app_shape(heroku_app, app_shape, num_workers) click.echo() click.echo(click.style('# Create Admin User', bold=True, fg='blue')) click.echo('You will need an initial admin user to start configuring your new MetaCI site. Enter a password for the admin user and one will be created for you') password = click.prompt('Password', hide_input=True, confirmation_prompt=True) # Create the admin user click.echo() click.echo(click.style('Creating admin user:'******'yellow')) command = 'python manage.py autoadminuser {FROM_EMAIL}'.format(**env) admin_output, dyno = heroku_app.run_command(command, printout=True, env={'ADMINUSER_PASS': password}) click.echo(admin_output.splitlines()[-1:]) # Create the admin user's API token click.echo() click.echo(click.style('Generating API token for admin user:'******'yellow')) command = 'python manage.py usertoken admin' token_output, dyno = heroku_app.run_command(command) for line in token_output.splitlines(): if line.startswith('Token: '): api_token = line.replace('Token: ', '', 1).strip() # Create the scheduled jobs needed by MetaCI click.echo() click.echo(click.style('Creating admin user:'******'yellow')) command = 'python manage.py metaci_scheduled_jobs' admin_output, dyno = heroku_app.run_command(command, printout=True, env={'ADMINUSER_PASS': password}) click.echo(admin_output.splitlines()[-1:]) if api_token: service = ServiceConfig({ 'url': env['SITE_URL'], 'token': api_token, 'app_name': check_data['app']['name'], }) config.keychain.set_service('metaci', service) click.echo(click.style('Successfully connected metaci to the new site at {SITE_URL}'.format(**env), fg='green', bold=True)) else: click.echo(click.style('Failed to create API connection to new metaci site at {SITE_URL}. Try metaci site connect'.format(**env), fg='red', bold=True))