-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from cedricvidal/lab-setup-script
Skillable Lab setup Python script
- Loading branch information
Showing
3 changed files
with
278 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,4 @@ | ||
## Azure Credentials: | ||
|
||
# CREDENTIALS | ||
|
||
++Username = "@lab.CloudPortalCredential(User1).Username" | ||
Password = "@lab.CloudPortalCredential(User1).Password" | ||
AzureEnvName = "[email protected]" | ||
Subscription = "@lab.CloudSubscription.Id"++ | ||
|
||
|
||
**If you are viewing this from the Skillable lab page** the above are your unique azure credentials. | ||
|
||
> **Note**: You will be asked to copy the above block in the lab later so keep this information readily available. | ||
**If you are viewing this from Github:** The above are not your credentials. They are placeholders. Your actual credentials can be seen on the Skillable lab page. | ||
|
||
*** | ||
|
||
### Welcome to this Microsoft workshop! | ||
### Welcome to the AI Tour and workshop WRK551! | ||
|
||
In this session, you will learn how to build the app, **Contoso Creative Writer**. This app will assist the marketing team at Contoso Outdoors in creating trendy, well-researched articles to promote the company’s products. | ||
|
||
|
@@ -43,9 +25,26 @@ To participate in this workshop, you will need: | |
3. Click the green **<> Create codespace** button at the bottom of the page. | ||
* This will open a pre-built Codespace on main. | ||
|
||
4. Once your Codespace is ready: | ||
> **🚧 IMPORTANT**: Do not open the GitHub Codespace on a fork of the repository, this would prevent you from using the prebuilt Codespace container image. Don't worry, you'll have the possibility to fork the repository later. | ||
4. Once your Codespace is ready, **run the following command**: | ||
|
||
``` | ||
./docs/workshop/lab_setup.py \ | ||
--username "@lab.CloudPortalCredential(User1).Username" \ | ||
--password "@lab.CloudPortalCredential(User1).Password" \ | ||
--azure-env-name "[email protected]" \ | ||
--subscription "@lab.CloudSubscription.Id" | ||
``` | ||
|
||
> [!IMPORTANT] | ||
> - **If you are viewing this from the Skillable lab page**: The above are your unique azure credentials. | ||
> - **If you are viewing this from Github**: The above are not your credentials. They are placeholders. Your actual credentials can be seen on the Skillable lab page. | ||
|
||
5. Once the previous script is complete: | ||
* In the file explorer look for the **docs** folder and in it open the **workshop** folder. | ||
* Open the **LAB-SETUP.ipynb** file. | ||
* Open the **workshop-1-intro.ipynb** file. | ||
* Follow the instructions to get going! | ||
|
||
Have fun building!🎉 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
#!/usr/bin/env python | ||
|
||
import rich_click as click | ||
import subprocess | ||
import os | ||
from functools import wraps | ||
from typing import List, Callable | ||
from click import style | ||
from pathlib import Path | ||
from inspect import signature | ||
|
||
# Add these constants near the top | ||
TEMP_FILE = Path.home() / '.lab_setup_progress' | ||
|
||
# Step registration | ||
steps: List[tuple[Callable, str]] = [] | ||
|
||
def blue(text: str): | ||
return style(text, fg="blue") | ||
|
||
def bold(text: str): | ||
return style(text, fg="bright_white", bold=True) | ||
|
||
def step(label: str): | ||
|
||
"""Decorator to register and label setup steps""" | ||
def decorator(func): | ||
@wraps(func) | ||
def wrapper(*args, step_number, total_steps, **kwargs): | ||
click.echo(f"\n{bold(f'Step {step_number}/{total_steps}')}: {blue(label)}") | ||
click.echo() | ||
return func(*args, **kwargs) | ||
steps.append((wrapper, label)) | ||
return wrapper | ||
return decorator | ||
|
||
@step("GitHub Authentication") | ||
def github_auth(*, force: bool = False): | ||
"""Authenticate with GitHub using the gh CLI tool""" | ||
# Only check authentication status if not forcing re-auth | ||
if not force: | ||
result = subprocess.run(['gh', 'auth', 'status'], | ||
capture_output=True, | ||
text=True, | ||
check=False) | ||
if result.returncode == 0: | ||
click.echo("Already authenticated with GitHub") | ||
return | ||
|
||
# Proceed with authentication | ||
process = subprocess.Popen( | ||
['gh', 'auth', 'login', | ||
'--hostname', 'github.com', | ||
'--git-protocol', 'https', | ||
'--web', | ||
'--scopes', 'workflow'], | ||
stdin=subprocess.PIPE, | ||
env={**os.environ, 'GITHUB_TOKEN': ''}, | ||
text=True | ||
) | ||
process.communicate(input='Y\n') | ||
|
||
@step("Fork GitHub Repository") | ||
def fork_repository(): | ||
"""Fork the current repository using the gh CLI tool""" | ||
# Check if upstream remote already exists | ||
result = subprocess.run(['git', 'remote', 'get-url', 'upstream'], | ||
capture_output=True, | ||
text=True, | ||
check=False) | ||
if result.returncode == 0: | ||
click.echo("Repository already has an upstream remote") | ||
return | ||
|
||
# Proceed with fork if no upstream remote exists | ||
subprocess.run(['gh', 'repo', 'fork', '--remote'], check=True) | ||
|
||
@step("Azure CLI Authentication") | ||
def azure_login(*, username: str = None, password: str = None, tenant: str = None, force: bool = False): | ||
# Only check authentication status if not forcing re-auth | ||
if not force: | ||
result = subprocess.run(['az', 'account', 'show'], | ||
capture_output=True, | ||
text=True, | ||
check=False) | ||
if result.returncode == 0: | ||
click.echo("Already authenticated with Azure CLI") | ||
return | ||
|
||
# Proceed with login if not authenticated or force=True | ||
login_cmd = ['az', 'login'] | ||
if username and password: | ||
login_cmd.extend(['-u', username, '-p', password]) | ||
if tenant: | ||
login_cmd.extend(['--tenant', tenant]) | ||
subprocess.run(login_cmd, check=True) | ||
|
||
@step("Azure Developer CLI Authentication") | ||
def azd_login(*, username: str = None, password: str = None, tenant: str = None, force: bool = False): | ||
"""Authenticate with Azure Developer CLI using device code""" | ||
|
||
# Display credentials if provided | ||
if username and password: | ||
opts = {'underline': True} | ||
click.echo(f"{style('When asked to ', **opts)}{style('Pick an account', **opts, bold=True)}{style(', hit the ', **opts)}{style('Use another account', **opts, bold=True)}{style(' button and enter the following:', **opts)}") | ||
click.echo(f"Username: {style(username, fg='blue', bold=True)}") | ||
click.echo(f"Password: {style(password, fg='blue', bold=True)}") | ||
click.echo() | ||
click.echo(f"{style('IMPORTANT', fg='red', reverse=True)}: {style('DO NOT use your personal credentials for this step!', fg='red', underline=True)}") | ||
click.echo() | ||
|
||
# Proceed with authentication | ||
login_cmd = ['azd', 'auth', 'login', '--use-device-code', '--no-prompt'] | ||
if tenant: | ||
login_cmd.extend(['--tenant-id', tenant]) | ||
subprocess.run(login_cmd, check=True) | ||
|
||
@step("Azure Developer CLI Environment Setup") | ||
def create_azd_environment(*, azure_env_name: str, subscription: str): | ||
# Check if environment already exists | ||
result = subprocess.run( | ||
['azd', 'env', 'list'], | ||
capture_output=True, | ||
text=True, | ||
check=True | ||
) | ||
|
||
if azure_env_name in result.stdout: | ||
click.echo(f"Environment '{azure_env_name}' already exists") | ||
return | ||
|
||
# Create new environment if it doesn't exist | ||
azd_cmd = [ | ||
'azd', 'env', 'new', azure_env_name, | ||
'--location', 'canadaeast', | ||
'--subscription', subscription | ||
] | ||
subprocess.run(azd_cmd, check=True) | ||
|
||
@step("Refresh AZD Environment") | ||
def refresh_environment(*, azure_env_name: str): | ||
subprocess.run([ | ||
'azd', 'env', 'refresh', | ||
'-e', azure_env_name, | ||
'--no-prompt' | ||
], check=True) | ||
|
||
@step("Export Environment Variables") | ||
def export_variables(): | ||
# Get the directory where the script is located and resolve .env path | ||
env_path = Path(__file__).parent.parent.parent / '.env' | ||
|
||
with open(env_path, 'w') as env_file: | ||
subprocess.run(['azd', 'env', 'get-values'], stdout=env_file, check=True) | ||
|
||
@step("Run Roles Script") | ||
def run_roles(): | ||
# Get the directory where the script is located | ||
script_dir = Path(__file__).parent | ||
roles_script = script_dir.parent.parent / 'infra' / 'hooks' / 'roles.sh' | ||
subprocess.run(['bash', str(roles_script)], check=True) | ||
|
||
@step("Execute Postprovision Hook") | ||
def run_postprovision(*, azure_env_name: str): | ||
process = subprocess.Popen( | ||
['azd', 'hooks', 'run', 'postprovision', '-e', azure_env_name], | ||
stdin=subprocess.PIPE, | ||
text=True | ||
) | ||
process.communicate(input='1\n') | ||
|
||
@click.command() | ||
@click.option('--username', help='Azure username/email for authentication') | ||
@click.option('--password', help='Azure password for authentication', hide_input=True) | ||
@click.option('--azure-env-name', required=True, help='Name for the new Azure environment') | ||
@click.option('--subscription', required=True, help='Azure subscription ID to use') | ||
@click.option('--tenant', help='Azure tenant ID') | ||
@click.option('--force', is_flag=True, help='Force re-authentication and start from beginning') | ||
@click.option('--step', type=int, help='Resume from a specific step number (1-based)') | ||
def setup(username, password, azure_env_name, subscription, tenant, force, step): | ||
""" | ||
Automates Azure environment setup and configuration. | ||
This command will: | ||
1. GitHub Authentication | ||
2. Fork GitHub Repository | ||
3. Azure CLI Authentication | ||
4. Azure Developer CLI Authentication | ||
5. Azure Developer CLI Environment Setup | ||
6. Refresh AZD Environment | ||
7. Export Environment Variables | ||
8. Run Roles Script | ||
9. Execute Postprovision Hook | ||
""" | ||
try: | ||
# Create parameters dictionary | ||
params = { | ||
'username': username, | ||
'password': password, | ||
'azure_env_name': azure_env_name, | ||
'subscription': subscription, | ||
'tenant': tenant, | ||
'force': force | ||
} | ||
|
||
# Determine starting step | ||
start_step = 0 | ||
if step is not None: | ||
if not 1 <= step <= len(steps): | ||
raise click.BadParameter(f"Step must be between 1 and {len(steps)}") | ||
start_step = step - 1 | ||
elif not force and TEMP_FILE.exists(): | ||
start_step = int(TEMP_FILE.read_text().strip()) | ||
if start_step >= len(steps): | ||
click.echo("\nAll steps were already successfully executed!") | ||
click.echo("Use --force to execute all steps from the beginning if needed.") | ||
return | ||
click.echo(f"\nResuming from step {blue(start_step + 1)}") | ||
|
||
# Execute all registered steps | ||
for index, entry in enumerate(steps): | ||
from inspect import signature | ||
# Skip steps that were already completed | ||
if index < start_step: | ||
continue | ||
|
||
step_func, _ = entry | ||
|
||
# Get the parameter names for this function | ||
sig = signature(step_func.__wrapped__) | ||
# Filter params to only include what the function needs | ||
step_params = { | ||
name: params[name] | ||
for name in sig.parameters | ||
if name in params | ||
} | ||
# Execute step and merge any returned dict into params | ||
result = step_func(step_number=index + 1, total_steps=len(steps), **step_params) | ||
if isinstance(result, dict): | ||
params.update(result) | ||
|
||
# Save progress after each successful step | ||
TEMP_FILE.write_text(str(index + 1)) | ||
|
||
# Clean up temp file on successful completion | ||
if TEMP_FILE.exists(): | ||
TEMP_FILE.unlink() | ||
|
||
click.echo("\nSetup completed successfully!") | ||
|
||
except subprocess.CalledProcessError as e: | ||
click.echo(f"Error during setup: {str(e)}", err=True) | ||
raise click.Abort() | ||
|
||
if __name__ == '__main__': | ||
setup() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
click | ||
rich-click |