Class: Makit::Azure::BlobStorage

Inherits:
Object
  • Object
show all
Defined in:
lib/makit/azure/blob_storage.rb

Class Method Summary collapse

Class Method Details

.delete_blobs(path) ⇒ Object

Delete all blobs in a storage path

Parameters:

  • path (String)

    The storage path (e.g., ‘$web/apps/portal/v0.1.0’)



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/makit/azure/blob_storage.rb', line 91

def self.delete_blobs(path)
  validate_storage_credentials!
  puts "  Deleting existing files in #{path}/".colorize(:yellow)

  cmd = [
    "az storage blob delete-batch",
    "--source '#{path}'",
    "--account-name #{}",
    "--account-key '#{}'",
    "--pattern '*'",
    "2>/dev/null",
  ].join(" ")

  system(cmd)
  # Don't fail if path doesn't exist (first deployment)
  true
end

.list_blobs(path) ⇒ Array<String>

List blobs in a storage path

Parameters:

  • path (String)

    The storage path (e.g., ‘$web’ or ‘$web/apps/portal’)

Returns:

  • (Array<String>)

    Array of blob names/paths (immediate children only, flat listing)



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/makit/azure/blob_storage.rb', line 163

def self.list_blobs(path)
  validate_storage_credentials!

  # Parse path to extract container name and prefix
  # Path format: "container" or "container/prefix/path"
  path_parts = path.split("/", 2)
  container_name = path_parts[0]
  prefix = path_parts[1] || ""

  # Build Azure CLI command
  cmd_parts = [
    "az storage blob list",
    "--container-name '#{container_name}'",
    "--account-name #{}",
    "--account-key '#{}'",
    "--output json",
    "--query '[].name'",
    "2>/dev/null",
  ]

  # Add prefix if specified
  unless prefix.empty?
    cmd_parts.insert(4, "--prefix '#{prefix}/'")
  end

  cmd = cmd_parts.join(" ")

  # Execute command and capture output
  output = `#{cmd}`.strip
  exit_code = $?.exitstatus

  # Handle command failure
  # Check exit code - non-zero indicates command failure
  if exit_code != 0
    raise "Failed to list blobs from Azure Storage"
  end

  # Parse JSON output
  begin
    require "json"
    # Empty string is not valid JSON and indicates command failure
    # Valid empty result from Azure CLI would be "[]", not ""
    if output.empty?
      raise "Failed to list blobs from Azure Storage"
    end
    blob_data = JSON.parse(output)
  rescue JSON::ParserError => e
    # Invalid JSON indicates parsing failure
    raise "Failed to parse Azure CLI output: #{e.message}"
  end

  # Return empty array if no blobs found
  return [] if blob_data.nil? || blob_data.empty?

  # Extract blob names from JSON array
  # JSON format from Azure CLI: [{"name":"path/to/blob"}, ...] or ["name1", "name2", ...]
  blob_names = if blob_data.first.is_a?(Hash)
                blob_data.map { |item| item["name"] }
              else
                blob_data
              end

  # Filter to get only immediate children (flat listing)
  prefix_with_slash = prefix.empty? ? "" : "#{prefix}/"
  immediate_children = []

  blob_names.each do |blob_name|
    # Remove prefix if specified to get relative path
    relative_path = if prefix_with_slash.empty?
                      blob_name
                    else
                      # Remove prefix from blob name
                      blob_name.sub(/^#{Regexp.escape(prefix_with_slash)}/, "")
                    end

    # Get only immediate children (first level after prefix)
    # Split by '/' and take first part, but preserve trailing slash if present
    parts = relative_path.split("/", 2)
    first_part = parts[0]
    
    # If the original blob_name had a trailing slash or ends with "/", preserve it
    # Check if this is a directory (ends with /) in the original name
    is_directory = blob_name.end_with?("/") || (parts.length > 1 && relative_path.end_with?("/"))
    first_part += "/" if is_directory && !first_part.end_with?("/")

    # Add to results if not already included (avoid duplicates)
    immediate_children << first_part unless immediate_children.include?(first_part)
  end

  immediate_children
end

.retrieve_from_key_vault(key) ⇒ String?

Retrieve a secret from Azure Key Vault Only attempts retrieval if Azure CLI is authenticated (az login has been performed)

Parameters:

  • key (String)

    The secret key name (e.g., “AZURE_STORAGE_ACCOUNT”)

Returns:

  • (String, nil)

    The secret value, or nil if not found or Azure CLI not authenticated



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/makit/azure/blob_storage.rb', line 31

def self.retrieve_from_key_vault(key)
  # Check if Azure Key Vault is configured
  keyvault_name = ENV["AZURE_KEYVAULT_NAME"]
  return nil unless keyvault_name && !keyvault_name.empty?
  
  # Check if Azure CLI is authenticated
  require_relative "../secrets/azure_key_vault"
  return nil unless Makit::Secrets::AzureKeyVault.azure_cli_authenticated?
  
  # Try to retrieve from Key Vault
  begin
    require_relative "../secrets/azure_secrets"
    azure_secrets = Makit::Secrets::AzureSecrets.new(keyvault_name: keyvault_name)
    value = azure_secrets.get(key)
    return value if value && !value.empty?
  rescue => e
    # Silently fail - Key Vault retrieval is optional
    # This allows the code to work without Key Vault if env vars are set
  end
  
  nil
end

.static_website_endpointString

Get static website endpoint URL for the storage account

Returns:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/makit/azure/blob_storage.rb', line 137

def self.static_website_endpoint
  validate_storage_credentials!

  unless Cli.available?
    # Fallback if Azure CLI not available
    return "https://#{}.z22.web.core.windows.net"
  end

  # Try to get the static website endpoint from Azure
  cmd = "az storage account show --name #{} --query 'primaryEndpoints.web' --output tsv 2>/dev/null"
  endpoint = `#{cmd}`.strip

  if endpoint.nil? || endpoint.empty?
    # Fallback: construct URL from storage account name
    # This assumes the standard format, but may not work for all storage accounts
    endpoint = "https://#{}.z22.web.core.windows.net"
    puts "  Warning: Could not retrieve static website endpoint, using default format".colorize(:yellow)
  end

  # Remove trailing slash if present
  endpoint.chomp("/")
end

.storage_accountString?

Get Azure Storage account from environment variable or Azure Key Vault

Returns:

  • (String, nil)

    The storage account name, or nil if not found



9
10
11
12
13
14
15
# File 'lib/makit/azure/blob_storage.rb', line 9

def self.
  # First try environment variable
  return ENV["AZURE_STORAGE_ACCOUNT"] if ENV["AZURE_STORAGE_ACCOUNT"] && !ENV["AZURE_STORAGE_ACCOUNT"].empty?
  
  # Fall back to Azure Key Vault if available (requires az login)
  retrieve_from_key_vault("AZURE_STORAGE_ACCOUNT")
end

.storage_account_keyString?

Get Azure Storage account key from environment variable or Azure Key Vault

Returns:

  • (String, nil)

    The storage account key, or nil if not found



19
20
21
22
23
24
25
# File 'lib/makit/azure/blob_storage.rb', line 19

def self.
  # First try environment variable
  return ENV["AZURE_STORAGE_ACCOUNT_KEY"] if ENV["AZURE_STORAGE_ACCOUNT_KEY"] && !ENV["AZURE_STORAGE_ACCOUNT_KEY"].empty?
  
  # Fall back to Azure Key Vault if available (requires az login)
  retrieve_from_key_vault("AZURE_STORAGE_ACCOUNT_KEY")
end

.upload_blobs(source, destination) ⇒ Object

Upload files to Azure Storage

Parameters:

  • source (String)

    Local directory to upload from

  • destination (String)

    Storage path destination (e.g., ‘$web/apps/portal/v0.1.0’)



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/makit/azure/blob_storage.rb', line 112

def self.upload_blobs(source, destination)
  validate_storage_credentials!
  puts "  Uploading files from #{source} to #{destination}/".colorize(:green)

  unless Dir.exist?(source)
    raise "Source directory does not exist: #{source}"
  end

  cmd = [
    "az storage blob upload-batch",
    "--destination '#{destination}'",
    "--source #{source}",
    "--account-name #{}",
    "--account-key '#{}'",
    "--overwrite",
  ].join(" ")

  unless system(cmd)
    raise "Failed to upload blobs to Azure Storage"
  end
  true
end

.validate_storage_credentials!Object

Validate Azure Storage credentials are set Checks environment variables first, then Azure Key Vault (if az login has been performed)



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/makit/azure/blob_storage.rb', line 56

def self.validate_storage_credentials!
   = 
  key = 
  
  if .nil? || .empty?
    error_msg = "AZURE_STORAGE_ACCOUNT is not set"
    # Provide helpful message about Key Vault fallback
    if ENV["AZURE_KEYVAULT_NAME"] && !ENV["AZURE_KEYVAULT_NAME"].empty?
      require_relative "../secrets/azure_key_vault"
      if Makit::Secrets::AzureKeyVault.azure_cli_authenticated?
        error_msg += " (checked environment variables and Azure Key Vault '#{ENV["AZURE_KEYVAULT_NAME"]}')"
      else
        error_msg += " (Azure Key Vault available but 'az login' not performed)"
      end
    end
    raise error_msg
  end
  
  if key.nil? || key.empty?
    error_msg = "AZURE_STORAGE_ACCOUNT_KEY is not set"
    # Provide helpful message about Key Vault fallback
    if ENV["AZURE_KEYVAULT_NAME"] && !ENV["AZURE_KEYVAULT_NAME"].empty?
      require_relative "../secrets/azure_key_vault"
      if Makit::Secrets::AzureKeyVault.azure_cli_authenticated?
        error_msg += " (checked environment variables and Azure Key Vault '#{ENV["AZURE_KEYVAULT_NAME"]}')"
      else
        error_msg += " (Azure Key Vault available but 'az login' not performed)"
      end
    end
    raise error_msg
  end
end